10. Imperative Example
Given a User "dave" exists with password "secret"
And I am not logged in
When I navigate to the home page
Then I am redirected to the login form
....
11. Refactored to be Declarative
Given I am an unauthenticated User
When I attempt to view some restricted content
Then I am shown a login form
When I authenticate with valid credentials
….
13. private UserDescription user;
@Given("^ a User … $")
public void a_user(String name, password) {
user = new UserBuilder()
.withName(name)
.withPassword(password).build();
}
14. private UserDescription user;
private UserBuilder userBuilder = new UserBuilder();
@Given("^I am an … $")
public void unauthenticated_user() {
user = userBuilder.build();
}
@Given("^an email address $")
public void user_email(String emailAddress) {
userBuilder.withEmail(emailAddress);
user = userBuilder.build();
}
15. public class IssueBuilder {
String summary = “Default Summary”;
Project project = new ProjectBuilder().build();
String reporter = new UserBuilder().build();
…
public IssueBuilder withProject(Project project) {
this.project = project;
return this;
}
public IssueDescription build() {…}
}
16. Issue issue = new IssueBuilder()
.withSummary(“Icon hover bug”).build();
.withReporter(
new UserBuilder().withName(“Alan”)
.build())
.build();
17. public class IssueBuilder {
…
Project project = null;
…
public IssueDescription build() {
if (project == null) {
project = new ProjectBuilder().build();
}
…
}
}
To isolate data between tests, script cleans and populates the databaseRequires direct access to the database
SQL Scripts are brittle to changePortability issues between database vendorsHigh maintenance costDifficult to create dataHard write by handSmall sets of data created – often reused across many tests.Run Application, add data and create Script from Database DumpLots of small variationsFor large sets of tests data schema changes are expensiveCan’t be used with Concurrent / Parallel Test ExecutionIgnores business rules on data – Script my load data that does not follow the correct business rules, cause test to fail. Hard to debugLimits reuse of Test artifactsCan’t access databases on production systems - NO smoke testing deployments –
Our Applications expose API’s all the timeREST, SOAP and RMI to name a fewUse them to create our test dataOn Demand – Given StepDevelopers maintain the public API – receives free maintenanceCode reuse – Code can be reused for Testing the REST services.Tests can be reused for Smoke testing deploymentsAs it doesn't clean the database. Requires generated data to be uniqueIgnores system state - the same tests can be repeatedly executed with the same result
Public API code - Large messy code for creating test data on demandNice abstraction is requiredReadable test setup codeCode reuseEasy and low maintenanceNat PryceCreate a builder for each class or resourceHas an instance variable for each constructor parameterInitialises its instance variables to commonly used or safe valuesHas a `build` method that creates a new object using the values in its instance variablesHas "chainable" public methods for overriding the values in its instance variables.
Simplest example of a builder used in Unit testsAll builders feature a build method - No parametersReturns value objectInstead we would make REST/API calls to our
Sensible defaults Click - Can be overriddenWritten a fluent styleEasy to read
Returns object describing the remotely created datae.g. unique IDIssueBuilder would return the Key of the created Issue.Even if the Any other useful information that is required by the test
The imperative style uses highly reusable granular steps which outlines much of the user interface. This binds the scenario to that UI and requires more design decisions made up front.Makes the Scenario brittle to minor changes in UI when no Behaviour or functionality has changed.
Each step states an idea.Sometimes it’s not really clear precisely what is being doneThe username and password detail has been pushed inside the test implementationWe don’t care about these details in this example – It doesn’t add anythingUse sensible defaults for these values – Data Builder also uses sensible defaults.
RelationshipsRefactor build method to lazily initialize
Sensible defaults Can be overriddenWritten a fluent styleEasy to readEmbedding builders
As we are creating remote objects, defaults should lazily initialized to avoid performance penalty
ID’s are best generated - previously run the same test suite and have not cleared the database, manually created unique ID’s could already be in use.Randomness - Repeating the test should not generate the same IDWe have found Hashes work quite well for Alphanumeric generationIssue – generates unique ID in the dataProject – requires us to generate a unique IDProject ID is up to 10 characters long and Alpha onlyGenerated IDs avoid clashes with stale dataHashes – MD5Seeding data generation ImportantTest method name useful componentRandom numberBusiness rules – Data length and typeAlpha only string generator
Different product versions/configurationsDifferent data formatsDifferent devicesCan be supported by the Abstract Factory patternCreate families of builders to support them
Avoid SQLUse exposed API’sGenerate Unique dataData Builder PatternFactories for your Data Builders
Test Data Builders: an alternate to the Object Mother patternCreate objects with complex constructors in Unit TestsHas an instance variable for each constructor parameterInitialises its instance variables to commonly used or safe valuesHas a `build` method that creates a new object using the values in its instance variablesHas "chainable" public methods for overriding the values in its instance variables.