Test Driven Development in Java
I’ve recently been looking a bit deeper into Test Driven development (TDD). You know, where you write a unit test first, and then write the code to fix it. I’ve been really impressed with how clean the code is that I’ve written this way, particularly in terms of its simplicity and low coupling. That got me looking for a testing framework for a Rails website I’m working on, and there I came across Cucumber. Cucumber is a Behaviour Driven Development ( BDD ) testing tool.
One of the principles of BDD is “Business and Technology should refer to the same system in the same way”, which to my limited interpretation means that business analysts should be able to read test cases.
What is really cool about cucumber is that it allows you to specify scenarios in English (or French, Sotho, etc).
Consider this example:
Scenario: Admin users should be able to edit all profiles
Given a website user Admin who is an administrator
And a website user Joe who is a user
When a user logs in as Admin
And navigates to the User Profile page
Then the page should allow editing of Joe’s profile
And the page should allow editing of Admin’s profile
Scenario: Normal users should only be able to edit their own profiles
Given a website user Admin who is an administrator
And a website user Joe who is a user
When a user logs in as Joe
And navigates to the User Profile page
Then the page should allow editing of Joe’s profile
And the page should not allow editing of Admin’s profile
Pretty simple right? You can read it and know exactly what the system should do under a given scenario. There’s nothing technical there at all. In fact, the only required syntax for Cucumber are the bits in green, which must start each line. You must have a Scenario, Given, When, Then. You can have as many And’s as you need under each section to specify additional Given’s/When’s/Then’s. This structure also forces a very clear, and definite way of thinking about a problem onto you – if you can’t specify it in this format (Given, When, Then) then you can’t test it (with Cucumber ;)!
Obviously there’s a chunk of work that needs to happen behind the scenes to make these scenarios run – we’ll get to that later. What’s got me excited is that this seems a very approachable ‘language’ for non-technical people to read (or in my more optimistic phases, even write) and understand.
Imagine a project where the business analysts write scenarios like these, and have a dashboard of what scenarios are working, and which not. And developers spend their time making these tests pass, safe in the knowledge that once all the scenarios pass, their work complies with the spec, and is working! Of course that’s Utopia, but Cucumber is a step in that direction. Even if it only goes as far as allowing business analysts to read what has actually been tested and translated into functionality from scenarios which are written by developers, I think it would still add a lot of value to the communication chasm we so often find between the people who define what they want, and the people who’s job’s it is to try deliver it to them.
Now Cucumber comes from the Ruby on Rails world, and there are some great plugins for it to simulate browsers, and do real integration level testing. There’s also a plugin that allows you to run it (via JRuby) in the JVM, and test Java code using Cuke4Duke, and you can do web integration testing using WebDriver/Selenium. I can’t claim to be an expert on any of this stack (or even Cucumber yet for that matter), but here’s an description of how to set up the classic calculator example to whet your appetite.
Install Maven
I have hangups against Maven from previous projects, but lets not get into that. You can also use Ant and Ivy, but the Maven way is much easier to explain.
Download Maven (Version 3.0.2 worked fine for me) and extract it somewhere.
Add the bin folder to your path
If you’re behind a proxy, set your ~/.m2/settings.xml
as described here
Install Cucumber & Cuke4Duke
Thanks to:
Cuke4Duke Maven config
Cuke4Duke pom.xml example
Using Cucumber tests with Maven and Java
- Create a working directory for this example, and add the following pom.xml to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>guruhut</groupId> <artifactId>cuke4duke-java-example</artifactId> <version>0.0.1</version> <packaging>jar</packaging> <name>Cucumber for Java: Example</name> <properties> <cuke4duke.version>0.4.3</cuke4duke.version> <!-- Behind a proxy? --> <!-- see http://wiki.github.com/aslakhellesoy/cuke4duke/installing-gems --> <!-- and http://github.com/aslakhellesoy/cuke4duke/issues/issue/36 --> <http.proxy>http://localhost:9999</http.proxy> </properties> <repositories> <repository> <id>codehaus</id> <url>http://repository.codehaus.org</url> </repository> <repository> <id>cukes</id> <url>http://cukes.info/maven</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>cukes</id> <url>http://cukes.info/maven</url> </pluginRepository> </pluginRepositories> <dependencies> <dependency> <groupId>cuke4duke</groupId> <artifactId>cuke4duke</artifactId> <version>${cuke4duke.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.picocontainer</groupId> <artifactId>picocontainer</artifactId> <version>2.11.2</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>cuke4duke</groupId> <artifactId>cuke4duke-maven-plugin</artifactId> <version>${cuke4duke.version}</version> <configuration> <jvmArgs> <!-- Debugging. See http://wiki.github.com/aslakhellesoy/cuke4duke/debug-cuke4duke-steps --> <!--jvmArg>-Xdebug</jvmArg> <jvmArg>-Xnoagent</jvmArg> <jvmArg>-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=4000</jvmArg> <jvmArg>-Xmx384m</jvmArg--> <jvmArg>-Dcuke4duke.objectFactory=cuke4duke.internal.jvmclass.PicoFactory</jvmArg> <jvmArg>-Dfile.encoding=UTF-8</jvmArg> </jvmArgs> <!-- You may not need all of these arguments in your own project. We have a lot here for testing purposes... --> <cucumberArgs> <cucumberArg>--require ${basedir}/target/test-classes</cucumberArg> </cucumberArgs> <gems> <gem>install cuke4duke --version ${cuke4duke.version}</gem> <!-- Behind a proxy? --> <gem>install cuke4duke --version ${cuke4duke.version} --http-proxy ${http.proxy}</gem> </gems> </configuration> <executions> <execution> <id>run-features</id> <phase>integration-test</phase> <goals> <goal>cucumber</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> |
- This is a minimal configuration for maven using Cucumber for intetgration tests. All you need to do is change the project group (groupId), identifier (artifactId) and name (name). Also check the section on proxy setup – Its enabled by default because I need it.
- Create a
features
folder under your project root. This is where we’ll add features shortly.
From the project root folder (where the pom.xml is), run the following command:
mvn -Dcucumber.installGems=true integration-test
- This will download the maven plugins required to run the project, Cuke4Duke so we can write some features, JRuby to run Cucumber, and Cucumber and its dependencies. Depending on your connection speed and whether you’ve run Maven before, this could take a while.
You should see something like:
1 2 3 4 5 |
[INFO] -- cuke4duke-maven-plugin:0.4.3:cucumber (run-features) @ cuke4duke-java-example -- [INFO] 0 scenarios [INFO] 0 steps [INFO] 0m0.000s [INFO] --------------------------------------------------------------------- |
[INFO] BUILD SUCCESS
[INFO]
1 |
Adding Features
Create a features/calculator.feature
containing the following:
Feature: Simple Calculator example
As a big number cruncher user
I want to be able to perform arithmetic
So that I can make lots of money
Now lets try testing our feature:
mvn integration-test
You should see something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
[INFO] -- cuke4duke-maven-plugin:0.4.3:cucumber (run-features) @ cuke4duke-java-example -- [INFO] Feature: Simple Calculator example [INFO] As a big number cruncher user [INFO] I want to be able to perform arithmetic [INFO] So that I can make lots of money [INFO] [INFO] Scenario: Add 0 # features/calculator.feature:6 [INFO] Given a calculator # features/calculator.feature:7 [INFO] When I add 1 and 0 # features/calculator.feature:8 [INFO] Then the result should be 1 # features/calculator.feature:9 [INFO] [INFO] 1 scenario (1 undefined) [INFO] 3 steps (3 undefined) [INFO] 0m0.024s [INFO] [INFO] You can implement step definitions for undefined steps with these snippets: [INFO] [INFO] Given /^a calculator$/ do [INFO] pending # express the regexp above with the code you wish you had [INFO] end [INFO] [INFO] When /^I add (\d+) and (\d+)$/ do |arg1, arg2| [INFO] pending # express the regexp above with the code you wish you had [INFO] end [INFO] [INFO] Then /^the result should be (\d+)$/ do |arg1| [INFO] pending # express the regexp above with the code you wish you had [INFO] end [INFO] [INFO] If you want snippets in a different programming language, just make sure a file [INFO] with the appropriate file extension exists where cucumber looks for step definitions. [INFO] [INFO] --------------------------------------------------------------------- |
[INFO] BUILD SUCCESS
[INFO]
What that’s saying is that you have 1 scenario that cannot be completed because it contains undefined steps, and that there are 3 undefined steps.
It then gives you some examples on how to create the necessary step definitions. Unfortunately they are in Ruby, but as soon as we have the first java example, you’ll see that the suggested solutions are converted to Java.
Defining Steps
Create a src/test/java/cukes/CalculatorSteps.java (The package doesn’t matter):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package cukes; import cuke4duke.annotation.I18n.EN.Given; import cuke4duke.annotation.I18n.EN.Then; import cuke4duke.annotation.I18n.EN.When; import cuke4duke.annotation.Pending; import java.math.*; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.*; public class CalculatorSteps { @Given("^a calculator$") @Pending public void setUpCalculator() { } } |
Rerun the integration tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[INFO] -- cuke4duke-maven-plugin:0.4.3:cucumber (run-features) @ cuke4duke-java-example -- [INFO] Feature: Simple Calculator example [INFO] As a big number cruncher user [INFO] I want to be able to perform arithmatic [INFO] So that I can make lots of money [INFO] [INFO] Scenario: Add 0 # features/calculator.feature:6 [INFO] Given a calculator # CalculatorSteps.setUpCalculator() [INFO] TODO (Cucumber::Pending) [INFO] features/calculator.feature:7:in `Given a calculator' [INFO] When I add 1 and 0 # features/calculator.feature:8 [INFO] Then the result should be 1 # features/calculator.feature:9 [INFO] [INFO] 1 scenario (1 pending) [INFO] 3 steps (2 undefined, 1 pending) [INFO] 0m0.070s [INFO] [INFO] You can implement step definitions for undefined steps with these snippets: [INFO] [INFO] @When("^I add 1 and 0$") [INFO] @Pending [INFO] public void iAdd1And0() { [INFO] } [INFO] [INFO] @Then("^the result should be 1$") [INFO] @Pending [INFO] public void theResultShouldBe1() { [INFO] } [INFO] [INFO] --------------------------------------------------------------------- |
[INFO] BUILD SUCCESS
[INFO]
You’ll see we now have 1 pending step i.e. there’s only a stub for the step for now. The @Pending annotation allows you to work on translating the steps into method calls first, and then worry about their implementation later (without forgetting to do them) Also, the examples are now Java because Cuke4Duke has figured out what language we’re using.
Define the rest of the steps:
1 2 3 4 5 6 7 8 |
@When("^I add 1 and 0$") @Pending public void addTwoNumbers() { } @Then("^the result should be 1$") @Pending public void theResultShouldBe() { } |
Your features should pass with 1 scenario pending.
Lets add some code to the test (Remember to take out the @Pending):
1 2 3 4 5 |
Calculator calc; @Given("^a calculator$") public void setUpCalculator() { calc = new Calculator(); } |
Now we’re into familiar TDD territory. Start adding code until the tests pass
And create our simple implementation class in src/main/java/cukes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package cukes; import java.math.*; public class Calculator { public BigDecimal add(BigDecimal value1, BigDecimal value2) { return value1.add(value2); } } Run the tests again – now we've got 1 passed, 1 pending & 1 skipped step Finish off the rest of the test: public class CalculatorSteps { Calculator calc; BigDecimal lastResult; @Given("^a calculator$") public void setUpCalculator() { calc = new Calculator(); } @When("^I add 1 and 0$") public void addTwoNumbers() { lastResult = calc.add(new BigDecimal(1), new BigDecimal(0)); } @Then("^the result should be 1$") public void theResultShouldBe() { assertThat(lastResult, is(new BigDecimal(1))); } } |
I’m sure some of you are screaming about all these hard-coded values, and wondering what’s the point of defining everything twice, once in the feature, and again in the step class. Hang in there, we’ll tidy it up in a minute.
Now lets add another scenario to our calculator.feature:
Running the tests tells us we need to add new steps. Instead of hard coding each scenario, lets DRY, and use RegEx to group out the variables on our existing step methods:
Change the RegEx to something like:
1 2 |
@When("^I add (\\d*) and (\\d*)$") @Then("^the result should be (\\d*)$") |
Run your tests again
I got:
1 2 3 4 5 6 |
[INFO] Scenario: Add 0 # features/calculator.feature:6 [INFO] Given a calculator # CalculatorSteps.setUpCalculator() [INFO] When I add 1 and 0 # CalculatorSteps.addTwoNumbers() [INFO] java.lang.ArrayIndexOutOfBoundsException: 0 (NativeException) [INFO] features/calculator.feature:8:in `When I add 1 and 0' [INFO] Then the result should be 1 # CalculatorSteps.theResultShouldBe() |
This error message is a bit cryptic, and it confused me for a while until I remembered that the RegEx groups are converted into method arguments, so we need change our methods as follows:
1 2 3 4 5 6 7 8 9 |
@When("^I add (\\d*) and (\\d*)$") public void addTwoNumbers(BigDecimal value1, BigDecimal value2) { lastResult = calc.add(value1, value2); } @Then("^the result should be (\\d*)$") public void theResultShouldBe(BigDecimal expectedResult) { assertThat(lastResult, is(expectedResult)); } |
Cuke4Duke does automatic conversions from String to your argument type.
Run the tests again, and you should see that you’ve now defined a testing language where the business analysts can add scenarios to their hearts content. Until something breaks, or the BA’s need more syntax, the developer’s job is done!
That concludes our trivial introduction to Cucumber for Java. I hope that it’s shown just how expressive tests can (and should be). I also hope that next time you have develop something, you’ll have a go at expressing it as set of a Given/When/Then’s – even if you never actually write it down, let alone run it – just to see how it affects your thinking.