2. Agenda
What will we cover today
How to name your tests
Hamcrest Matchers
Parameterized tests
JUnit Rules
Mockito
Spock
Geb
Web testing
Thucydides and easyb
4. What’s in a name
Name your tests well
"What's in a name? That which we call a rose
By any other name would smell as sweet."
Romeo and Juliet (II, ii, 1-2)
5. What’s in a name
The 10 5 Commandments of Test Writing
I. Don’t say “test, say “should” instead
II. Don’t test your classes, test their behaviour
III. Test class names are important too
IV. Structure your tests well
V. Tests are deliverables too
6. What’s in a name
Don’t use the word ‘test’ in your test names
testBankTransfer()
testWithdraw()
testDeposit()
7. What’s in a name
Do use the word ‘should’ in your test names
testBankTransfer()
testWithdraw()
tranferShouldDeductSumFromSourceAccountBalance()
testDeposit()
transferShouldAddSumLessFeesToDestinationAccountBalance()
depositShouldAddAmountToAccountBalance()
8. What’s in a name
Your test class names should represent context
When is this behaviour applicable?
What behaviour are we testing?
9. What’s in a name
Write your tests consistently
‘Given-When-Then’ or ‘Arrange-Act-Assert’ (AAA)
@Test
public void aDeadCellWithOneLiveNeighbourShouldRemainDeadInTheNextGeneration() {
String initialGrid = "...n" +
".*.n" + Prepare the test data (“arrange”)
"...";
String expectedNextGrid = "...n" +
"...n" +
"...n"; Do what you are testing (“act”)
Universe theUniverse = new Universe(seededWith(initialGrid));
theUniverse.createNextGeneration();
String nextGrid = theUniverse.getGrid();
Check the results (“assert”)
assertThat(nextGrid, is(expectedNextGrid));
}
10. What’s in a name
Tests are deliverables too - respect them as such
Refactor, refactor, refactor!
Clean and readable
11. Express Yourself with Hamcrest
Why write this...
import static org.junit.Assert.*;
...
assertEquals(10000, calculatedTax, 0);
when you can write this...
import static org.hamcrest.Matchers.*;
...
assertThat(calculatedTax, is(10000));
“Assert that are equal 10000 and calculated tax (more or less)” ?!
Don’t I just mean “assert that calculated tax is 10000”?
12. Express Yourself with Hamcrest
With Hamcrest, you can have your cake and eat it!
assertThat(calculatedTax, is(expectedTax));
Readable asserts
String color = "red";
assertThat(color, is("blue"));
Informative errors
String[] colors = new String[] {"red","green","blue"};
String color = "yellow";
assertThat(color, not(isIn(colors)));
Flexible notation
13. Express Yourself with Hamcrest
More Hamcrest expressiveness
String color = "red";
assertThat(color, isOneOf("red",”blue”,”green”));
List<String> colors = new ArrayList<String>();
colors.add("red");
colors.add("green");
colors.add("blue");
assertThat(colors, hasItem("blue"));
assertThat(colors, hasItems("red”,”green”));
assertThat(colors, hasItem(anyOf(is("red"), is("green"), is("blue"))));
15. Home-made Hamcrest Matchers
Customizing Hamcrest matchers
You can build your own by combining existing Matchers...
Create a dedicated Matcher for the Stakeholder class
List stakeholders = stakeholderManager.findByName("Health");
Matcher<Stakeholder> calledHealthCorp = hasProperty("name", is("Health Corp"));
assertThat(stakeholders, hasItem(calledHealthCorp));
Use matcher directly with hasItem()
“The stakeholders list has (at least) one item with
the name property set to “Health Corp””
16. Home-made Hamcrest Matchers
Writing your own matchers in three easy steps!
public class WhenIUseMyCustomHamcrestMatchers {
@Test
public void thehasSizeMatcherShouldMatchACollectionWithExpectedSize() {
List<String> items = new ArrayList<String>();
items.add("java");
assertThat(items, hasSize(1));
}
}
We want something like this...
I want a matcher that checks the size of a collection
17. Home-made Hamcrest Matchers
Writing your own matchers in three easy steps!
public class HasSizeMatcher extends TypeSafeMatcher<Collection<? extends Object>> {
private Matcher<Integer> matcher;
Extend the TypeSafeMatcher class
public HasSizeMatcher(Matcher<Integer> matcher) {
this.matcher = matcher; Provide expected values in
} the constructor
public boolean matchesSafely(Collection<? extends Object> collection) {
return matcher.matches(collection.size());
} Do the actual matching
public void describeTo(Description description) {
description.appendText("a collection with a size that is");
matcher.describeTo(description);
} Describe our expectations
}
So let’s write this Matcher!
18. Home-made Hamcrest Matchers
Writing your own matchers in three easy steps!
import java.util.Collection;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
public class MyMatchers { Use a factory class to store your matchers
@Factory
public static Matcher<Collection<? extends Object>> hasSize(Matcher<Integer> matcher){
return new HasSizeMatcher(matcher);
}
}
All my custom matchers go in a special Factory class
19. Home-made Hamcrest Matchers
Writing your own matchers in three easy steps!
import static com.wakaleo.gameoflife.hamcrest.MyMatchers.hasSize;
import static org.hamcrest.MatcherAssert.assertThat;
public class WhenIUseMyCustomHamcrestMatchers {
@Test
public void thehasSizeMatcherShouldMatchACollectionWithExpectedSize() {
List<String> items = new ArrayList<String>();
items.add("java");
assertThat(items, hasSize(1));
}
}
Hamcrest-style error messages
20. Home-made Hamcrest Matchers
But wait! There’s more!
@Test
public void weCanUseCustomMatchersWithOtherMatchers() {
List<String> items = new ArrayList<String>();
items.add("java");
assertThat(items, allOf(hasSize(1), hasItem("java")));
}
Combining matchers
@Test
public void weCanUseCustomMatchersWithOtherMatchers() {
List<String> items = new ArrayList<String>();
items.add("java");
items.add("groovy");
assertThat(items, hasSize(greaterThan(1)));
}
Nested matchers
22. Using Parameterized Tests
Parameterized tests - for data-driven testing
Take a large set of test data, including an expected result
Define a test that uses the test data
Verify calculated result against expected result
{2, 0, 0}
{2, 1, 2}
{2, 2, 4}
{2, 3, 6}
{2, 4, 8}
x=a*b
{2, 5, 10}
Test
{2, 6, 12}
{2, 7, 14}
... Verify
Data
24. Using Parameterized Tests
Parameterized tests with JUnit 4.8.1 Income Expected Tax
$0.00 $0.00
What you need: $10,000.00 $1,250.00
Some test data $14,000.00 $1,750.00
$14,001.00 $1,750.21
A test class with matching fields $45,000.00 $8,260.00
$48,000.00 $8,890.00
And some tests $48,001.00 $8,890.33
$65,238.00 $14,578.54
And an annotation $70,000.00 $16,150.00
public class TaxCalculatorDataTest {
@RunWith(Parameterized.class) $70,001.00 $16,150.38
public classdouble income;
private TaxCalculatorDataTest {
$80,000.00 $19,950.00
private double expectedTax;
income;
private double expectedTax; $100,000.00 $27,550.00
public TaxCalculatorDataTest(double income, double expectedTax) {
super();
this.income = income;
public TaxCalculatorDataTest(double income, double expectedTax) {
this.income = income;
this.expectedTax = expectedTax;
super();
} this.expectedTax = expectedTax;
this.income = income;
} } this.expectedTax = expectedTax;
}
@Test
public void shouldCalculateCorrectTax() {...}
@Test
} public void shouldCalculateCorrectTax() {...}
}
25. Using Parameterized Tests
How it works This is a parameterized test
Income Expected Tax
@RunWith(Parameterized.class) $0.00 $0.00
public class TaxCalculatorDataTest { The @Parameters annotation $10,000.00 $1,250.00
private double income;
private double expectedTax;
indicates the test data $14,000.00 $1,750.00
@Parameters $14,001.00 $1,750.21
public static Collection<Object[]> data() { $45,000.00 $8,260.00
return Arrays.asList(new Object[][] {
{ 0.00, 0.00 }, $48,000.00 $8,890.00
{ 10000.00, 1250.00 }, { 14000.00, 1750.00 }, $48,001.00 $8,890.33
{ 14001.00, 1750.21 }, { 45000.00, 8260.00 },
{ 48000.00, 8890.00 }, { 48001.00, 8890.33 }, $65,238.00 $14,578.54
{ 65238.00, 14578.54 }, { 70000.00, 16150.00 },
$70,000.00 $16,150.00
{ 70001.00, 16150.38 }, { 80000.00, 19950.00 },
{ 100000.00, 27550.00 }, }); $70,001.00 $16,150.38
}
$80,000.00 $19,950.00
public TaxCalculatorDataTest(double income, double expectedTax) { $100,000.00 $27,550.00
super();
this.income = income;
this.expectedTax = expectedTax; The constructor takes the
} fields from the test data
@Test
public void shouldCalculateCorrectTax() {
TaxCalculator calculator = new TaxCalculator(); The unit tests use data
double calculatedTax = calculator.calculateTax(income);
assertThat(calculatedTax, is(expectedTax)); from these fields.
}
}
26. Using Parameterized Tests
Parameterized Tests in Eclipse
Income Expected Tax
Run the test only once $0.00 $0.00
$10,000.00 $1,250.00
Eclipse displays a result for each data set $14,000.00 $1,750.00
$14,001.00 $1,750.21
$45,000.00 $8,260.00
$48,000.00 $8,890.00
$48,001.00 $8,890.33
$65,238.00 $14,578.54
$70,000.00 $16,150.00
$70,001.00 $16,150.38
$80,000.00 $19,950.00
$100,000.00 $27,550.00
27. Using Parameterized Tests
Example: using an Excel Spreadsheet
@Parameters
public static Collection spreadsheetData() throws IOException {
InputStream spreadsheet = new FileInputStream("src/test/resources/aTimesB.xls");
return new SpreadsheetData(spreadsheet).getData();
}
29. JUnit Rules
The Temporary Folder Rule
public class LoadDynamicPropertiesTest {
Create a temporary folder
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private File properties;
@Before Prepare some test data
public void createTestData() throws IOException {
properties = folder.newFile("messages.properties");
BufferedWriter out = new BufferedWriter(new FileWriter(properties));
// Set up the temporary file
out.close();
}
Use this folder in the tests
@Test
public void shouldLoadFromPropertiesFile() throws IOException {
DynamicMessagesBundle bundle = new DynamicMessagesBundle();
bundle.load(properties);
// Do stuff with the temporary file
}
}
The folder will be deleted afterwards
30. JUnit Rules
The ErrorCollector Rule
Report on multiple error conditions in a single test
public class ErrorCollectorTest {
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test Two things went wrong here
public void testSomething() {
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
String result = doStuff();
collector.checkThat(result, not(containsString("Oh no, not again")));
}
Check using Hamcrest matchers
private String doStuff() {
return "Oh no, not again";
}
}
31. JUnit Rules
The ErrorCollector Rule
Report on multiple error conditions in a single test
public class ErrorCollectorTest {
@Rule All three error messages are reported
public ErrorCollector collector = new ErrorCollector();
@Test
public void testSomething() {
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
String result = doStuff();
collector.checkThat(result, not(containsString("Oh no, not again")));
}
private String doStuff() {
return "Oh no, not again";
}
}
32. JUnit Rules
The Timeout Rule
Define a timeout for all tests
public class GlobalTimeoutTest {
@Rule
public MethodRule globalTimeout = new Timeout(1000);
@Test No test should take longer than 1 second
public void testSomething() {
for(;;);
}
Oops
@Test
public void testSomethingElse() {
}
}
33. Parallel tests
Setting up parallel tests with JUnit and Maven
<project...> Needs Surefire 2.5
<plugins>
...
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration> ‘methods’, ‘classes’, or ‘both’
<parallel>methods</parallel>
</configuration>
</plugin>
</plugins>
...
<build>
<dependencies>
<dependency> Needs JUnit 4.8.1 or better
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</build>
...
</project>
34. Continuous Testing
Continuous Tests with Infinitest
Infinitest is a continuous test tool for Eclipse and IntelliJ
Runs your tests in the background when you save your code
35. Continuous Testing
Using Infinitest
Whenever you save your file changes, unit tests will be rerun
Failing test
Project containing an error
Error message about the failed test
41. Spock - Unit BDD in Groovy
Specifications in Groovy
import spock.lang.Specification; Specifications, not tests
class RomanCalculatorSpec extends Specification {
def "I plus I should equal II"() {
given:
def calculator = new RomanCalculator()
when:
def result = calculator.add("I", "I")
then:
result == "II"
}
}
42. Spock - Unit BDD in Groovy
Specifications in Groovy BDD-style
def "I plus I should equal II"() {
when: "I add two roman numbers together"
def result = calculator.add("I", "I")
then: "the result should be the roman number equivalent of their sum"
result == "II"
}
43. Spock - Unit BDD in Groovy
Specifications in Groovy BDD-style
def "I plus I should equal II"() {
when: "I add two roman numbers together"
def result = calculator.add("I", "I")
then: "the result should be the roman number equivalent of their sum"
result == "II"
}
This is the assert
I plus I should equal II(com.wakaleo.training.spocktutorial.RomanCalculatorSpec)
Time elapsed: 0.33 sec <<< FAILURE!
Condition not satisfied:
result == "II"
| |
I false
1 difference (50% similarity)
I(-)
I(I)
at com.wakaleo.training.spocktutorial
.RomanCalculatorSpec.I plus I should equal II(RomanCalculatorSpec.groovy:17)
44. Spock - Unit BDD in Groovy
Specifications in Groovy
def "The lowest number should go at the end"() {
when:
def result = calculator.add(a, b)
then:
result == sum Data-driven testing
where:
a | b | sum
"X" | "I" | "XI"
"I" | "X" | "XI"
"XX" | "I" | "XXI"
"XX" | "II" | "XXII"
"II" | "XX" | "XXII"
}
45. Spock - Unit BDD in Groovy
Specifications in Groovy
def "Messages published by the publisher should only be received by active subscribers"() {
given: "a publisher"
def publisher = new Publisher()
and: "some active subscribers"
Subscriber activeSubscriber1 = Mock() Setting up mocks
Subscriber activeSubscriber2 = Mock()
activeSubscriber1.isActive() >> true
activeSubscriber2.isActive() >> true
publisher.add activeSubscriber1
publisher.add activeSubscriber2
and: "a deactivated subscriber"
Subscriber deactivatedSubscriber = Mock()
deactivatedSubscriber.isActive() >> false
publisher.add deactivatedSubscriber
when: "a message is published"
publisher.publishMessage("Hi there") Asserts on mocks
then: "the active subscribers should get the message"
1 * activeSubscriber1.receive("Hi there")
1 * activeSubscriber2.receive({ it.contains "Hi" })
and: "the deactivated subscriber didn't receive anything"
0 * deactivatedSubscriber.receive(_)
}
46. Geb - Groovy Page Objects
DSL for WebDriver web testing
import geb.*
Browser.drive("http://google.com/ncr") { Concise expression language
assert title == "Google"
// enter wikipedia into the search field
$("input", name: "q").value("wikipedia")
// wait for the change to results page to happen
// (google updates the page without a new request)
waitFor { title.endsWith("Google Search") }
// is the first link to wikipedia? Higher level than WebDriver
def firstLink = $("li.g", 0).find("a.l")
assert firstLink.text() == "Wikipedia"
// click the link
firstLink.click() Power asserts
// wait for Google's javascript to redirect
// us to Wikipedia
waitFor { title == "Wikipedia" }
}
48. User stories
As a job seeker
I want to find jobs in relevant categories
So that I can find a suitable job
Features/Epics
49. User stories
As a job seeker
I want to find jobs in relevant categories
So that I can find a suitable job
Acceptance criteria
☑
The
job
seeker
can
see
available
categories
on
the
home
page
☑
The
job
seeker
can
look
for
jobs
in
a
given
category
☑
The
job
seeker
can
see
what
category
a
job
belongs
to
50. User stories
As a job seeker
I want to find jobs in relevant categories
So that I can find a suitable job
Acceptance criteria
☑
The
job
seeker
can
see
available
categories
on
the
home
page
☑
The
job
seeker
can
look
for
jobs
in
a
given
category
☑
The
job
seeker
can
see
what
category
a
job
belongs
to
scenario "A job seeker can see the available job categories on the home page",
{
when "the job seeker is looking for a job",
then "the job seeker can see all the available job categories"
}
Automated acceptance test
51. scenario "A job seeker can see the available job categories on the home page",
{
when "the job seeker is looking for a job",
then "the job seeker can see all the available job categories"
}
Automated acceptance test
Implemented development tests Implemented acceptance tests
52. The art of sustainable web tests
or how not to have web tests like this
53. The Three Ways of Automated Web Testing
Record/Replay
Scripting
Page Objects
60. A sample Page Object
A Page Object
FindAJobPage
lookForJobsWithKeywords(values : String)
getJobTitles() : List<String>
61. A sample Page Object
public class FindAJobPage extends PageObject {
An implemented
WebElement keywords;
WebElement searchButton;
Page Object
public FindAJobPage(WebDriver driver) {
super(driver);
}
public void lookForJobsWithKeywords(String values) {
typeInto(keywords, values);
searchButton.click();
}
public List<String> getJobTitles() {
List<WebElement> tabs = getDriver()
.findElements(By.xpath("//div[@id='jobs']//a"));
return extract(tabs, on(WebElement.class).getText());
}
}
62. A sample Page Object
public class WhenSearchingForAJob {
@Test
public void searching_for_a_job_should_display_matching_jobs() {
FindAJobPage page = new FindAJobPage();
page.open("http://localhost:9000");
page.lookForJobsWithKeywords("Java");
assertThat(page.getJobTitles(), hasItem("Java Developer"));
}
}
A test using this
Page Object
68. scenario "A job seeker can see the available job categories on the home page",
{
when "the job seeker is looking for a job",
then "the job seeker can see all the available job categories"
}
Automated
scenario "The user can see the available job categories on the home page",
{
when "the job seeker is looking for a job",
{
job_seeker.open_jobs_page()
}
then "the job seeker can see all the available job categories",
{
job_seeker.should_see_job_categories "Java Developers", "Groovy Developers"
}
}
Implemented
JobSeekerSteps
JobSeekerSteps
JobSeekerSteps
open_jobs_page()
open_jobs_page()
open_jobs_page()
should_see_job_categories(String...
categories)
should_see_job_categories(String...
categories)
... should_see_job_categories(String...
categories)
...
...
Step libraries
69. scenario "The user can see the available job categories on the home page",
{
when "the job seeker is looking for a job",
{
job_seeker.open_jobs_page()
}
then "the job seeker can see all the available job categories",
{
job_seeker.should_see_job_categories "Java Developers", "Groovy Developers"
}
}
Implemented Tests
JobSeekerSteps
JobSeekerSteps
JobSeekerSteps
open_jobs_page()
open_jobs_page()
open_jobs_page()
should_see_job_categories(String...
categories)
should_see_job_categories(String...
categories)
... should_see_job_categories(String...
categories)
...
...
Step libraries
Page Objects
78. Defining your acceptance tests
scenario "A
job seeker
{ can see the
available j
ob categori
when "the j es on the h
ob seeker i ome page",
then "the j s looking f
ob seeker c o r a j o b ",
} an see all
the availab
le job cate
gories"
High level requ
irements...
scenario "The administrator adds a new category to the system",
{
given "a new category needs to be added to the system",
when "the administrator adds a new category",
then "the system should confirm that the category has been created",
and "the new category should be visible to job seekers",
}
{
scenario "The admini
strator deletes a ca
tegory from the syst
em",
...defined in business terms
given "a category ne
eds to be deleted",
when "the administra
tor deletes a catego
then "the system will ry",
confirm that the cate
and "the deleted cate gory has been delete
gory should no longer d",
} be visible to job se
eker s",
focus on business value
79. Organizing your requirements
Features public class Application {
@Feature
public class ManageCompanies {
public class AddNewCompany {}
public class DeleteCompany {}
public class ListCompanies {}
}
@Feature
public class ManageCategories {
public class AddNewCategory {}
public class ListCategories {}
public class DeleteCategory {}
}
@Feature Stories
public class BrowseJobs {
public class UserLookForJobs {}
public class UserBrowsesJobTabs {}
}
}
80. Implementing your acceptance tests
using "thucydides" We are testing this story
thucydides.uses_steps_from AdministratorSteps
thucydides.uses_steps_from JobSeekerSteps
thucydides.tests_story AddNewCategory
An acceptance criteria
scenario "The administrator adds a new category to the system",
{
given "a new category needs to be added to the system",
{
Narrative style
administrator.logs_in_to_admin_page_if_first_time()
administrator.opens_categories_list()
} Step through an
when "the administrator adds a new category", example
{
administrator.selects_add_category()
administrator.adds_new_category("Scala Developers","SCALA")
}
then "the system should confirm that the category has been created",
{
administrator.should_see_confirmation_message "The Category has been created"
}
and "the new category should be visible to job seekers",
{ Still high-level
job_seeker.opens_jobs_page()
job_seeker.should_see_job_category "Scala Developers"
}
}
81. Some folks prefer JUnit...
@RunWith(ThucydidesRunner.class)
@Story(AddNewCategory.class) Thucydides handles the
public class AddCategoryStory {
web driver instances
@Managed
public WebDriver webdriver;
@ManagedPages(defaultUrl = "http://localhost:9000")
public Pages pages;
@Steps
public AdministratorSteps administrator;
@Steps
Using the same steps
public JobSeekerSteps job_seeker;
@Test
public void administrator_adds_a_new_category_to_the_system() {
administrator.logs_in_to_admin_page_if_first_time();
administrator.opens_categories_list();
administrator.selects_add_category();
administrator.adds_new_category("Java Developers","JAVA");
administrator.should_see_confirmation_message("The Category has been created");
job_seeker.opens_job_page();
job_seeker.should_see_job_category("Java Developers");
} Tests can be pending
@Pending @Test
public void administrator_adds_an_existing_category_to_the_system() {}
}
82. Defining your test steps
public class AdministratorSteps extends ScenarioSteps { A step library
@Step
public void opens_categories_list() {
AdminHomePage page = getPages().get(AdminHomePage.class);
page.open();
page.selectObjectType("Categories");
} High level steps...
@Step
public void selects_add_category() {
CategoriesPage categoriesPage = getPages().get(CategoriesPage.class);
categoriesPage.selectAddCategory();
}
@Step
public void adds_new_category(String label, String code) {
EditCategoryPage newCategoryPage = getPages().get(EditCategoryPage.class);
newCategoryPage.saveNewCategory(label, code);
}
...implemented
@Step
public void should_see_confirmation_message(String message) {
with Page Objects
AdminPage page = getPages().get(AdminPage.class);
page.shouldContainConfirmationMessage(message);
}
@StepGroup ...or with other steps
public void deletes_category(String name) {
opens_categories_list();
displays_category_details_for(name);
deletes_category();
}
}
83. Defining your page objects
public class EditCategoryPage extends PageObject {
@FindBy(id="object_label")
WebElement label; Provides some useful
@FindBy(id="object_code")
utility methods...
WebElement code;
@FindBy(name="_save")
WebElement saveButton;
public EditCategoryPage(WebDriver driver) {
super(driver);
}
public void saveNewCategory(String labelValue, String codeValue) {
typeInto(label, labelValue);
typeInto(code, codeValue);
saveButton.click();
}
} but otherwise a normal
WebDriver Page Object
84. Data-driven testing
Test data
categories.csv
public class DataDrivenCategorySteps extends ScenarioSteps {
Test steps
public DataDrivenCategorySteps(Pages pages) {
super(pages);
}
private String name;
private String code;
@Steps
public AdminSteps adminSteps;
public void setCode(String code) {...}
public void setName(String name) {...}
@Step
public void add_a_category() {
adminSteps.add_category(name, code);
}
}
85. Data-driven testing
Test data
categories.csv
public class DataDrivenCategorySteps extends ScenarioSteps {
Test steps
public DataDrivenCategorySteps(Pages pages) {
super(pages);
}
private String name;
private String code;
@Steps
public AdminSteps adminSteps;
@Steps
public void setCode(String code) {...}
public DataDrivenCategorySteps categorySteps;
public void setName(String name) {...}
@Step @Test
public void add_a_category() {
Call this step for
public void adding_multiple_categories() throws IOException {
adminSteps.add_category(name, code);
steps.login_to_admin_page_if_first_time();
}
}
steps.open_categories_list(); each row
withTestDataFrom("categories.csv").run(categorySteps).add_a_category();
}