Ryan Pollard ryan.pollard.world Dylan Just dylan@techtangents.com - - PowerPoint PPT Presentation
Ryan Pollard ryan.pollard.world Dylan Just dylan@techtangents.com - - PowerPoint PPT Presentation
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,
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
Not found? Not reachable? Could return null/exception? Passing Driver around!
def changeLocale(locale: Locale) (implicit config: TestConfig, driver: WebDriver): Unit = { goToUserSettings typeInCurrentPassword setLocale(locale) clickOk waitForPageToGoAway }
Exceptions Implicits
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
A Read values from the browser
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*
IO[A]
!
A
def apply[A](a: => A): IO[A]
unsafePerformIO: 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 A write / accumulate return
An Action may pass or fail.
Either
Err \/ A Err
- Test Assertion Failed
- Could Not Find Element
- Wrong Element Type
- Kersploded
- Other
A
Reader IO Writer Either A
IO Reader Writer Either A
EitherT WriterT ReaderT IO A
EitherT WriterT ReaderT IO
Monad Transformers form monads for stacks of 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)
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) )
Create
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
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();
Executing
*** 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)
"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)
Composing Executing
Log
final case class Log[A](list: DList[Node[A]]) final case class Node[A](label: A, children: Log[A])
Nodes Log Action
logOnly("pen")
pen
logOnly("pen")
pen apple
logOnly("apple")
*>
apple
logOnly("apple") *> logOnly("pen")
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 :)