The Bed Nap application dynamically generates 2 pages with links between them; it's about time we started doing some testing before it gets out of hand!
Bounce is a library written specifically for testing BedSheet web applications so it would be a shame not to use it! So the first thing we should do is download afBounce
and add it to our build.fan
.
fanr install -r http://eggbox.fantomfactory.org/fanr "afBounce 1.1"
- Create a Test Directory
- Create a Base Test Class
- Test View Details are Shown
- Test Index Details are Shown
- Test Link to View Page
- Test All in F4
- Source Code
Create a Test Directory
It's a Fantom convention that tests are kept in a separate directory to the main code, so we'll keep out test code in a root project directory called test/
.
Using F4
Right click on the Bed Nap project and select New -> Fantom Source Folder
. Enter the name test
and click OK
. F4 will automatically update build.fan
for you.
Without F4
Add test/
to the srcDirs
list in build.fan
. Make sure the directory has a trailing slash.
srcDirs = [`fan/`, `fan/components/`, `fan/pages/`, `test/`]
Create a Base Test Class
Before we write any actual tests we're going to write an abstract base class. The base class will override setup()
and teardown()
and do the useful things we need to do in every web test, namely startup and shutdown BedSheet!
using afBounce::BedServer using afBounce::BedClient abstract class WebTest : Test { BedClient? client override Void setup() { server := BedServer(AppModule#).startup server.inject(this) client = server.makeClient } override Void teardown() { client.shutdown } }
You can think of BedServer
as a fake BedSheet server. It starts up BedSheet in a similar fashion to normal, only without the overhead of connecting to real network ports. BedClient
may then used to make fake HTTP requests to our fake server.
Note the line server.inject(this)
, that allows all subclasses to define @Inject
fields just like an IoC service!
When we run our tests, we want to be in complete control of our Visit
data, so we would rather not have the createSampleData
function run at BedSheet startup. So lets remove it! If we contribute our own TestModule
, we can do just that. Our new WebTest
class now looks like:
using afIoc using afBounce::BedServer using afBounce::BedClient abstract class WebTest : Test { BedClient? client override Void setup() { server := BedServer(AppModule#).addModule(TestModule#).startup server.injectIntoFields(this) client = server.makeClient } override Void teardown() { client.shutdown } } const class TestModule { Void onRegistryStartup(Configuration config) { config.remove("bednap.createSampleData") } }
Test View Details are Shown
The strategy for testing web pages involves navigating to the page, using CSS selectors to pinpoint specific HTML elements, and asserting they contain expected values.
To select HTML elements for testing, it's best (for guarding against future layout changes) to give each element in question its own unique ID. Only, as I may reuse an ID, I tend to use class attributes instead. I also give the class a t-
prefix so I know they're used for testing (and not for styling).
So let's update the ViewPage
HTML so it has some test CSS attributes:
... html.div("class='t-name'").w("${visit.name} said:").divEnd html.div("class='t-comment'").w(visit.comment).divEnd html.div("class='t-date'").w("on ${visit.date}").divEnd html.div("class='t-rate'").w("${visit.rating} / 5 stars").divEnd ...
This makes our test fairly straight forward - save some Visit
data and check it appears on the ViewPage
:
using afIoc::Inject using afBounce::Element class TestViewPage : WebTest { @Inject VisitService? visitService Void testDetailsAreShown() {// givenvisit := Visit("Butcher", Date(1969, Month.mar, 23), 5, "Sausages", 8) visitService.save(visit)// whenclient.get(`/view/8`)// thenElement(".t-name" ).verifyTextEq("Butcher said:") Element(".t-comment").verifyTextEq("Sausages") Element(".t-date" ).verifyTextEq("on 1969-03-23") Element(".t-rate" ).verifyTextEq("5 / 5 stars") } }
TIP: To print out the rendered HTML, use this line:
echo(client.lastResponse.asStr)
Test Index Details are Shown
To test the table on the IndexPage
we can do the same thing. We can use the visit ID as part of the class attributes to ensure uniqueness. So in IndexPage
:
visitService.all.each { html.tr html.td("class='t-v${it.id}-name'").w(it.name).tdEnd html.td("class='t-v${it.id}-date'").w(it.date).tdEnd html.td("class='t-v${it.id}-rate'").w(it.rating).tdEnd html.td html.a(`/view/${it.id}`, "class='t-v${it.id}-view'").w("view").aEnd html.tdEnd html.trEnd }
And the test:
using afIoc::Inject using afBounce::Element class TestIndexPage : WebTest { @Inject VisitService? visitService Void testMultipleVisitSummariesAreShown() {// givenvisit1 := visitService.save(Visit("User 1", Date(2000, Month.jan, 1), 1, "", 1)) visit2 := visitService.save(Visit("User 2", Date(2000, Month.jan, 2), 2, "", 2)) visit3 := visitService.save(Visit("User 3", Date(2000, Month.jan, 3), 3, "", 3))// whenclient.get(`/`)// thenElement(".t-v1-name").verifyTextEq("User 1") Element(".t-v1-date").verifyTextEq("2000-01-01") Element(".t-v1-rate").verifyTextEq("1") Element(".t-v2-name").verifyTextEq("User 2") Element(".t-v2-date").verifyTextEq("2000-01-02") Element(".t-v2-rate").verifyTextEq("2") Element(".t-v3-name").verifyTextEq("User 3") Element(".t-v3-date").verifyTextEq("2000-01-03") Element(".t-v3-rate").verifyTextEq("3") } }
(Note how we converted save()
into a builder method so we could inline the Visit
assignment.)
While the above test works - it's ugly! Having to assign unique IDs to everything is verbose and awkward, and it doesn't even test ordering on the page, just that the data exists.
Luckily, if a CSS selector occurs more than once in a document, then Bounce lets you access it like an array. So let's try discarding those Visit
IDs on IndexPage
:
... visitService.all.each { html.tr html.td("class='t-name'").w(it.name).tdEnd html.td("class='t-date'").w(it.date).tdEnd html.td("class='t-rate'").w(it.rating).tdEnd html.td html.a(`/view/${it.id}`, "class='t-view'").w("view").aEnd html.tdEnd html.trEnd } ...
And use the array accessors in the test:
Void testMultipleVisitSummariesAreShown() {// givenvisit1 := visitService.save(Visit("User 1", Date(2000, Month.jan, 1), 1, "", 1)) visit2 := visitService.save(Visit("User 2", Date(2000, Month.jan, 2), 2, "", 2)) visit3 := visitService.save(Visit("User 3", Date(2000, Month.jan, 3), 3, "", 3))// whenclient.get(`/`)// thenElement(".t-name")[0].verifyTextEq("User 1") Element(".t-date")[0].verifyTextEq("2000-01-01") Element(".t-rate")[0].verifyTextEq("1") Element(".t-name")[1].verifyTextEq("User 2") Element(".t-date")[1].verifyTextEq("2000-01-02") Element(".t-rate")[1].verifyTextEq("2") Element(".t-name")[2].verifyTextEq("User 3") Element(".t-date")[2].verifyTextEq("2000-01-03") Element(".t-rate")[2].verifyTextEq("3") }
Now our test is also checking the ordering of our data on the page.
Test Link to View Page
We added links to the ViewPage
from the IndexPage
, lets test that they work.
To do this we render the IndexPage
, click a known link, and check that the correct page has been rendered with the correct details:
Void testViewPageLink() {// givenvisit := visitService.save(Visit("Emma", Date(1969, Month.mar, 23), 5, "Sausages", 8))// whenclient.get(`/`) Link(".t-view").click// then// check we're on the right pageElement("title").verifyTextEq("Bed Nap View Page")// check the right details are displayedElement(".t-name").verifyTextContains("Emma") }
Test All in F4
For every Fantom project I have in F4 I usually also have a Test All
run configuration saved as a favourite. This is the one to run before you check in.
Source Code
All the source code for this tutorial is available on the Bed Nap Tutorial Bitbucket Repository.
Code for this particular article is available on the 05-Testing-With-Bounce branch.
Use the following commands to check the code out locally:
C:\> hg clone https://bitbucket.org/fantomfactory/bed-nap-tutorial C:\> cd bed-nap-tutorial C:\> hg update 05-Testing-With-Bounce
Don't forget, you can trial the finished tutorial application at Bed Nap.
Have fun!
Edits
- 5 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
- 5 Aug 2015 - Updated tutorial to use BedSheet 1.4.
- 1 Sep 2014 - Original article.