FW/1 Example Application - Testing your App with BDD and Integration Tests
18 Apr 2015This final article is probably the most interesting (and important!) in the series and details how to implement Behaviour Driven Development tests and Integration Tests on the Example Application using FW/1.
The links above provide excellent definitions on these different concepts, so I strongly suggest the reader to become familiar with them.
BDD, specifically, encompasses much more than just the tests - learning and implementing these concepts was incredibly rewarding, as it changed some of my stubborn ways of approaching development!
Getting Technical (Starting with Dependencies)
This project has two dependencies: testBox and CFSelenium.
These frameworks, respectively, allow us to implement BDD style tests, and integration tests using Selenium - which look so awesome, like if a ghost was hired to do nothing but QA tests!
Assuming you are using a development machine (don’t do this in a production environment) download and install both of these tools at your webserver’s root:
When in doubt, check the the Project Structure article. You can also choose another location if you want, but then you’ll have to create appropriate mappings.
Running Tests
Testing files are stored within the /tests
folder:
Application.cfc
- inherits settings from the main application, and then sets
some test-specific configs.
Please note that one of these settings is a “tests only” datasource, pointing to a clone of the default database. I’m still evaluating the pros and cons of this approach and, honestly, it’s not really something you need to replicate, but I left it this way for the sake of learning.
ìndex.cfm
- This is just a copy of the “test browser” that comes with TestBox.
It lets you see and choose the tests available, or run all of them.
/specs
- Each test is a CFC file stored in this folder. I gave them descriptive
names like Test_1_ExampleSpec.cfc
because I wanted them displayed in order (and
first tests to be the simplest), but in truth you can name them however you like
(I still suggest you be descriptive).
Accessing http://your-server/clipping/tests
should display a test browser page:
We start with Test_1_ExampleSpec.cfc
, a most basic test, just to show the standard structure
and progress to Test_6_Integration_Selenium.cfc
, in which we use CFSelenium to perform full Integration tests.
In a nutshell, Selenium starts a browser instance on your server and follows a “test script”, allowing TestBox to record what went as expected or not, and displaying the results:
Writing Tests (this was actually fun!)
The basic anatomy of a test can be seen in Test_1_ExampleSpec.cfc
component extends="testbox.system.BaseSpec"{
// executes before all suites
function beforeAll(){}
// executes after all suites
function afterAll(){}
// All suites go in here
function run( testResults, testBox ){
describe("A suite", function(){
it("contains a very simple spec", function(){
expect( true ).toBeTrue();
});
......
});
}
}
TestBox has excellent reference material at http://wiki.coldbox.org/wiki/TestBox.cfm, but to make things clear, here’s what’s happening:
-
Our component extends TestBox’s “BaseSpec” class, allowing us to use some of its methods
-
Function
beforeAll()
allows us to dictate what will happen before the tests run (similar to a setup() method in other testing frameworks). We could use this, for example, to insert test data into the database. -
Function
afterAll()
allows us to dictate what will happen after the tests run (similar to a tearDown() method in other testing frameworks). We could use this, for example, to remove test data from the database. -
Function
run()
initiates the testing itself, running one “suite” at a time -
Describe()
is how we define a test suite and its scope -
Finally,
it()
is a single spec in that suite, and it might contain one or more expectations, that will dictate whether this spec passed, or failed.
The tests included in the source code are very much self-explanatory and provide many examples on how to set up and accomplish things, from “unit” testing your UDFs, to databases and remote services.
Integration Tests with Selenium
CFSelenium is a CFC wrapper for Selenium functionality, making it easy to use it inside CFML code.
It allows you to script browser sessions and test the outputs and behaviours against what’s expected. We are using it on top of TestBox, so the syntax is pretty much the same as the one above, and the results can be reported along the other tests we’ve done.
Here’s how we set it up in tests/specs/Test_6_Integration_Selenium.cfc
:
component extends="testbox.system.BaseSpec"{
// executes before all suites
function beforeAll(){
// set url of APP installation
browserURL = application.testsBrowseURL;
// set browser to be used for testing
browserStartCommand = "*googlechrome";
// create a new instance of CFSelenium
selenium = createobject("component", "CFSelenium.selenium").init();
// start Selenium server
selenium.start(browserUrl, browserStartCommand);
// set timeout period to be used when waiting for page to load
timeout = 120000;
// rebuild current App
httpService = new http();
httpService.setUrl(browserURL & "/index.cfm?rebuild=true");
httpService.send();
// create some random title string (we will use this to delete the article later)
str_random_title = createUUID();
// text to be used in articles
str_default_text = repeatString("<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br>Integer nec nulla ac justo viverra egestas.</p>", 10);
}
// executes after all suites
function afterAll(){
selenium.stop();
selenium.stopServer();
}
Function beforeAll()
creates an instance of Selenium, telling it what browser
to use and what URL to start with (it also sets some variables that will be used
in the tests).
When everything is done afterAll()
stops the Selenium session and results are
displayed in the report.
The first actual tests makes sure that the starting page is titled “Clippings” (please note the BDD style language):
describe("Loading home page", function(){
it("Should load and have the correct title", function(){
selenium.open(browserUrl);
selenium.waitForPageToLoad(timeout);
expect( selenium.getTitle() ).toBe( "Clippings" );
});
});
Selenium opens the start URL and waits for the page to load. We then use
selenium.getTitle()
to extract the page’s title so we can compare it to our
expectation.
Simple, right?
Here’s a little more complex interaction:
//----------------------------------------------------------------------
// Testing forms
//----------------------------------------------------------------------
describe("Testing the clipping form:", function(){
it("Clicking -add an article- link should load the form page", function(){
selenium.open(browserURL);
selenium.waitForPageToLoad(timeout);
selenium.click("link=Add an Article");
selenium.waitForPageToLoad(timeout);
expect( selenium.isElementPresent("id=f_clipping") ).toBe( true );
expect( selenium.isElementPresent("id=published") ).toBe( true );
});
it("The app must validade form entry (leave article TEXT empty)", function(){
selenium.open(browserURL & "?action=clipping.form");
selenium.waitForPageToLoad(timeout);
selenium.type("id=clipping_titulo", "test");
selenium.type("id=clipping_texto", "");
selenium.click("id=btn_save");
selenium.waitForPageToLoad(timeout);
// didn't fill all the required fields...should return with error
expect( selenium.isTextPresent("Your article could not be posted!") ).toBe( true );
});
-
First test makes Selenium “click” on a link and sees if a “New Article” form was loaded.
-
Second one attempts to submit the form without filling all the required fields, making sure we get the apropriate error message.
For the sake of brevity I won’t detail all the other tests performed, but the CFC contains several different examples:
- Using CSS and XPATH to find elements and values
- Using Javascript to fill form elements
- Integrating CFML code for even more complex tests
Again, I have to give credit to Simon Bingham and his excellent Xindi CMS project. I feel incredibly lucky that he made that repository public so I could see how he dealt with several issues that showed up during development.
For more detailed information on this project, follow the other articles in this series:
- Project Structure
- Forms and Validation Patterns
- Use of User Defined Function Libraries
- Interaction with a Remote Service
- BDD and Integration Tests.
For the full source code, please visit the fw1-clipping github project page.