Ryan Pollard ryan.pollard.world Dylan Just dylan@techtangents.com
Flax A good source of Selenium
The Problem ● Legacy product needed testing. ● Scala codebase. ● Product UI was very complex. ● Needed simple tests.
Goals ● Nice, high-level language. ● Usable by testers. ● Clean syntax - readable by non-testers. ● Feedback on failures. ● To FP or not to FP?
!|script |com.ephox.webradar.fitnesse.fixtures.WebRadarFixture | |open url |http://myserver:10039/wps/portal | |click link having text |Log In | |enter text |username |in textbox having id|userID | |enter text |mypassword |in textbox having id|password | |click element having id |login.button.login | |verify text |wpsadmin |appears on page |
Anyone promoting tools where you can ‘program without programming’ is just selling you a terrible programming language.
Let's use a programming language!!!
Let's use a programming language!!!
Selenium Java Client ● No effect-tracking. ● Exceptions and nulls. ● Mutable data. ● Messy syntax. ● Java.
Selenium Java Client
Selenium Java Client Passing Driver around! Not reachable? Not found? Could return null/exception?
Implicits def changeLocale(locale: Locale) (implicit config : TestConfig, driver : WebDriver): Unit = { goToUserSettings typeInCurrentPassword setLocale(locale) clickOk waitForPageToGoAway Exceptions }
Different Shapes
FP
FP
FP
Flax Attacks
Inside the Flax Shack ● Import Specs2. ● Import Flax. ● Make tests go now.
Flax Strikes Back github.com/idempotency/investo
Thanks, Functional Programming. Thunktional Programming.
Code that describes things FP Code that does things
Describe: Testing with Selenium
We need a Selenium WebDriver object
Side effects: browser interaction
Logs: what actions were performed?
Tests may pass or fail
Read values from the browser A
A
A
"Action"
An Action takes a WebDriver as input.
Reader case class Reader[T, A] = Reader(f: T => A) Reader[Driver, A] Reader[T, A] T => A
An Action may perform effects on the browser
IO* def apply[A](a: => A): IO[A] IO[A] ! unsafePerformIO: A A
An Action may produce log messages.
Writer case class Writer[W, A] = Writer (run: (W, A)) Writer[Log[String], A] Writer[W, A] W write / accumulate return A
An Action may pass or fail.
Either Err \/ A Err • Test Assertion Failed • Could Not Find Element • Wrong Element Type • Kersploded • Other
A
IO Reader Writer Either A
IO Reader Writer Either A
EitherT WriterT ReaderT IO A
Monad EitherT Transformers WriterT form monads ReaderT for stacks of IO monadic things
case class Action(run: EitherT[ WriterT[ ReaderT[IO, Driver, ?], Log[String], ?], Err, A ])
What do we want to do with Actions? ● Create them. ● "Chain" them together ● Execute them as tests.
Create ● logOnly (String) ● fromSideEffect ((d: Driver) => ...) ● assert (x must_=== y) ● find (By) ● click (Elem)
Create def fromSideEffectWithLog [A]( msg : String, run : Driver => A): Action[A] = … def get (url: String): Action[Unit] = fromSideEffectWithLog ( "Opening URL: " + url , driver => driver .d. get ( url ) )
Chaining actions?
Monading def setTextBy(by: By, text: String): Action[Unit] = for { e <- find(by) _ <- clear(e) _ <- typeIn(e, text) _ <- click("submit") _ < assert } yield ()
Stack traces vs Logs
Executing driver.get( "https://twitter.com/" ); driver.findElement(By. xpath ( "//input[@name='session[username_or_email]']" )) .sendKeys(username); driver.findElement(By. xpath ( "//input[@name='session[password]']" )) .sendKeys(password); driver.findElement(By. xpath ( "//input[@value='Log on']" )).click(); doSomethingElse(); *** Element info: {Using=xpath, value=//input[@value='Log on']} ... at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:545) at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:319) at org.openqa.selenium.remote.RemoteWebDriver.findElementByXPath(RemoteWebDriver.java:421) at org.openqa.selenium.By$ByXPath.findElement(By.java:361) at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:311) at com.ephox.flax.it.MyTest.main(MyTest.java:20)
Composing "My test" >> { for { _ <- get ( "https://twitter.com/" ) _ <- typeInBy ( xpath ( "//input[@name='session[username_or_email]']" ), username ) _ <- typeInBy ( xpath ( "//input[@name='session[password]']" ), password ) _ <- clickBy ( xpath ( "//input[@value='Log in']" )) } yield () } ... a.runAsResult(driver) Executing
Log Action final case class Log[A](list: DList[Node[A]]) final case class Node[A](label: A, children: Log[A]) Log Nodes
logOnly("pen") pen
logOnly("apple") logOnly("pen") apple pen
*>
logOnly("apple") *> logOnly("pen") apple pen
nested ( "Open twitter homepage" , get ( "https://twitter.com/" )) Open twitter homepage Opening URL: https://twitter.com/
nested ( "Enter Credentials" , for { _ <- typeInBy ( xpath ( "//...." ), username ) _ <- typeInBy ( xpath ( "//...." ), password ) } Enter Credentials Type in by xpath... Type in by xpath...
def signInToTwitter: Action[Unit] = nested ( "Sign in to Twitter" , for { _ <- openTwitterHomepage _ <- enterCredentials _ <- clickLoginButton } yield ()) def openTwitterHomepage: Action[Unit] = nested ( "Open twitter homepage" , get ( "https://twitter.com/" )) def enterCredentials: Action[Unit] = nested ( "Enter Credentials" , for { _ <- typeInBy ( xpath ( "//input[@name='session[username_or_email]']" ), username ) _ <- typeInBy ( xpath ( "//input[@name='session[password]']" ), password ) } yield ()) def clickLoginButton: Action[Unit] = nested ( "Click login button" , clickBy ( xpath ( "//input[@value=' Lorg in ']" )))
Steps performed: - Signing in to Twitter - Open twitter homepage - Opening URL: https://twitter.com/ - Enter Credentials - Finding element: By.xpath: //input[@name='session[username_or_email]'] - Typing "flax_demo" in element: By.xpath: //input[@name='session[username_or_email]'] - Finding element: By.xpath: //input[@name='session[password]'] - Typing "flaxtwitter" in element: By.xpath: //input[@name='session[password]'] - Click login button - Finding element: By.xpath: //input[@value='Lorg in'] !!! Could not find element: By.xpath: //input[@value='Lorg in']
Conclusion ● FP gave practical benefits. ● Scala "for" comprehensions. ● Monad transformers - hard in Scala, but helpful ● Log tree. ● FP data types modelled key aspects of behaviour.
Ryan Pollard ryan.pollard.world Dylan Just dylan@techtangents.com
Workshop 1. Set up Intellij and sbt shell command. 2. "Getting started" instructions. 3. Test some things. Things to try: a. Open a site b. Click a button c. Type text d. Nest actions Example scenarios follow...
Test ephox.com https://www.ephox.com/ Click "compare rich text editors" Click the "learn more" link - this takes us to tinymce.com Execute some js to get the content from the tinymce instance Get the "quick start" html text.
Test docs https://www.tinymce.com/docs/ Search for "hybrid". Click on the links.
assertions Use FlaxAssertions.assert to assert some simple properties on the previous examples. Observe test passes and failures.
Implementing Actions Check out the Flax code from github Implement the api from the selenium "Select" in the "Selekt" wrapper class. See "Elem" for an example. Send a PR :)
Recommend
More recommend