Write readable
tests in java
About unit tests
The code samples:
The code samples are written in
java and kept very simple.
The ideas presented are,
however, not limited to java
development and should be
useful for most of the
programming languages on the
market.
Slides content:
I find readable tests a valuable
form of documentation in each
software project.
The slides contain some rules
that I believe make the tests
more maintainable and easy to
understand.
Hi,
I am Marian Wamsiedel and I
work as a java developer, mostly
in java enterprise projects.
You can find me at
marian.wamsiedel@gmail.com
What makes a unit test good
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Focused
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Be focused
Test a single functionality and do it right
Be focused
» Make sure that your test exercises a single
piece of functionality
» Usually a single assert pro test method will do
it. If you write more asserts, make sure they
test the same thing
» The test method name should be easy to find
and contains no „and's“
Be focused
» Avoid passing boolean flags to a checking
method. Write two test methods instead:
» private void checkUnderlyingSystemCalled(boolean imported){
if (imported) {
verify(oldSystem).calculate();
}else {
verify(newSystem).calculate();
}
» Not cool: it is difficult to read and violates the
single responsability principle
Be focused
» Better: write two methods with clear names
@Test
public void
shouldLoadPortedCustomerHistoryOverLegacySystem(){
Customer ported = customer().ported().build();
underTest.statisticsForCustomer(ported);
verify(oldSystem).loadHistory();
}
Be focused
» Better: write two methods with clear names
@Test
public void shouldLoadRegularCustomerHistoryOverNewSystem(){
Customer regular = customer().regular().build();
underTest.statisticsForCustomer(regular);
verify(newSystem).loadHistory();
}
Context unaware
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Be context unaware
Do not rely on the external collaborators
Be context unaware
System
Have a concentric view of the
system:
- the unit tests check the
functionality of the system
- the unit tests fake its
collaborators (mock/stub)
- the unit tests do not know
anything about the external world
Collaborators
first
sec
ond
External world
Be context unaware
» Never check the functionality of the collaborators
» Never call methods on real collaborators, always
mock, or stub them
» Do not talk to strangers: have absolutely no
knowledge about the collaborators of the
collaborators
Be context unaware
» There is something wrong if:
– Your tests fail to run outside of a suite and each
test needs to extend a base test class that
provides some setup
– You need to mock some global functionality, like:
authentication & authorization
– Your tests fail if they run in a different order
Compact
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Be compact
Write short code, without duplications
Be compact
» Create only relevant setup data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» Each test setup block should contain only the setup
data that really affects the test result, nothing more
» Avoid calling irrelevant "setters", or
"allArgsConstructors"
» Hide the "noise" information in factory methods, or
custom builders
create only relevant setup data
Be compact,
» Using allArgConstructor:
Person person = new Person("12343221", "Joe", "Doe",
new HomeAddress(...), true, "01.01.1970", true, ...);
assertThat(
price().for(person),
containsDiscount()); //why?
» Better:
assertThat(
price().for(person().withAge(60)),
containsDiscount());
create only relevant setup data
Be compact
» Create only relevant test data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» Setup data that affects the test output should be
visible. Do not hide relevant information in a helper
method, or a factory class
» The connection between input data and test output
should be clear and should document the production
code
expose relevant setup data
Be compact,
» assertThat(
price().for(person("Joe")),
containsDiscount()); //why?
» assertThat(
priceFor(person().withAge(17)),
containsDiscount()); //better
» assertThat(
priceFor(person().withAge(18)),
isRegular()); //better
expose relevant setup data
Be compact
» Create only relevant test data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» Prefer creating test data, over mocking
» Mocking the test data make setup methods long and
boring to read
» Use mocking only to fake the functionality of the
system collaborators, if you prefer mocking over
stubbing
create, don't mock test data
Be compact,
» Mock a teenager customer that lives in London:
» Address address = Mockito.mock(Address.class);
Customer customer = Mockito.mock(Customer.class);
when(address.city()).thenReturn("London");
when(customer.homeAddress()).thenReturn(address);
when(customer.age()).thenReturn(14);
underTest.doSomethingFor(customer);
create, don't mock test data
Be compact,
» Create a teenager user that lives in London:
» Customer = customer()
.withHomeAddress(
address().withCity(„London“))
.withAge(14);
underTest.doSomethingFor(customer);
create, don't mock test data
Be compact
» Create only relevant test data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» When creating a test suite, you probably need same,
or similar domain objects in different tests
» Creating each of them inside each test class where
needed leads to duplicated code. Duplication makes
maintenance difficult, boring and expensive
» There are two approaches that you might consider
create test data, options
Be compact,
» First approach: object mother [Fowler], test data
warehouse, test data factory
» Create a class, that exposes factory methods to
create the domain objects the tests need
» As the code base grows, it might be difficult to
extend and modify the class, since all the tests
use the same set of creational methods
create test data, object mother
Be compact,
» Second approach: let the domain classes expose
customized builders, that initialize the required fields
with standard data
» Let the tests initialize only the relevant fields
» This solution should be easier to maintain and
extend than the object mother, when the code base
grows
create test data, custom builders
Be compact,
» CustomerBuilder aCustomer(){
return Customer.builder()
.withName("Joe")
.withSecondName("Doe")
.withAddress(...)}
// in the test class
» Customer retired = aCustomer().bornIn(1950).build();
assertThat(
underTest.priceFor(retired),
isDiscounted());
create test data, custom builders
Be compact
» Create only relevant test data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» Design the domain beans as smart classes, that
encapsulate data and expose just part of it over its API
» The users of the domain bean should not have access
to the bean internals
» Offer the exposed information to the users,
don't let them search for it, repeteadly
calling accessor methods
smart domain beans, no data bags
Be compact,
» Use java beans only when required. Resist the
temptation to write getters and setters for each
private field
» Example: a customer can have more addresses:
home address, billing address, delivery address and
so on. Let's say we need
to check the city of the billing address
smart domain beans, no data bags
Be compact,
» The first approach, with Customer as java bean
// In the test class:
» Address billing = customer.getAddresses().stream()
.filter(address -> BILLING == address.type())
.findFirst()
.orElse(Address.EMPTY);
return billing.city();
// The test code gets longer, less readable, duplicated
» The test has full knowledge of the
customer class internals
smart domain beans, no data bags
Be compact,
» The second approach:
» class Customer {
private List<Address> addresses;
public Address billingAddress(){...}; // store the logic here
}
// In the test class
» when(repository.customer(1)).thenReturn(
customer().withBillingAddress(address().withCity(„London“));
Customer customer = underTest.customerWithId(1);
assertThat(
customer.billingAddress().city(),
is("London"));
smart domain beans, no data bags
Be compact
» Create only relevant test data
» Expose relevant setup data
» Create, don't mock test data
» Create test data, options
» Smart domain beans, no data bags
» Minimize code sharing
Be compact,
» If you really need to store some constants in an external
file, consider creating a class for each technical area, do
not store all test constants in a global class
» Consider also a mother object class for each technical
area
» Do not initialize all mocks in a global setup
method. You will end up with tests that rely on
initialization made in other test method
minimize code sharing
Be compact,
» Useful questions:
» if you need to refactor the test file, to move it into an
external project, how many files would you also need to
modify?
» if the tests run in a different order, will they still remain
green?
minimize code sharing
Declarative
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Be declarative, not imperative
Describe what, not how
Be declarative, not imperative
» The natural way to write java code is the imperative
one. It describes "how" the job is done and is driven by
actions, like: do, set, load, check
» The alternative is the declarative style, which describes
rather "what" is being done and the result of the
actions, hidding the internals of how the action is done
Be declarative, not imperative
» Check that a fresh made espresso is hot, imperative:
» goToTheCoffeMachine();
turnItOn();
makeSureItHasEnoughBeans();
…
pressTheEspressoButton();
waitUntilTheFinishMessageIsDisplayed();
assertThat(
getTheCoffee(),
isHot());
» The declarative approach:
» Espresso espresso = freshMadeEspresso();
assertThat(underTest, isHot());
» assertThat(
havingAFreshMadeEspresso(),
isAHotDrink());
Be declarative, not imperative
Strong names or none
Strong
names or
none
Declarative
Focused
Compact
Context
unaware
Strong names, or none
Make an impact, or be silent
» Some names are better than the others:
» name the system under test accordingly:
- „underTest“, „toTest“
- better than: „form“, „customer“, „mapper“
» generally, the single noun names are easier to read and
understand than the long ones:
- „repository“, not "shoppingCartRepository"
- „from“, not „serviceShoppingCartItem“
- „to“, not „domainShoppingCartItem“
Strong names, or none
» Avoid storing local vars if they don't bring anything new:
» Fruit apple = apple(); //nothing new
Fruit pear = pear();
Fruit peach = peach();
assertThat(
new FruitSalat(apple, pear, peach),
containsNo(Strawberry.class));
//inline the vars, if there is no loss of information
» assertThat(
new FruitSalat(apple(), pear(), peach()),
containsNo(Strawberry.class));
Strong names, or none
» Store local vars if they bring relevant information:
» Fruit regularProduct = apple();
Fruit bioProduct = pear();
Fruit localProduct = peach();
FruitSalat mixed = new FruitSalat(regularProduct, bioProduct, localProduct)
assertThat(
mixed.price(),
lowerThan(premiumPrice()));
Strong names, or none
Credits
- Growing Object-Oriented Software, Guided by Tests
(Steve Freeman, Nat Pryce)
- Clean Code, A Handbook of Agile Software
Craftsmanship (Robert C. Martin)
- Elegant Objects (Yegor Bugayenko)
- https://martinfowler.com/bliki/ObjectMother.html
Credits
» Presentation template by SlidesCarnival
» Photographs by Unsplash
» Photographs by Freepik:
– „Designed by Kjpargeter / Freepik“
– „Designed by jcomp / Freepik“
– „Designed by Kjpargeter / Freepik“
– „Designed by Mariia_Fr / Freepik“
THANKS!
You can find me for any questions at
marian.wamsiedel@gmail.com
Or over linkedin:
https://www.linkedin.com/in/marian-wamsiedel-4a1b792b/

Write readable tests

  • 1.
  • 2.
    About unit tests Thecode samples: The code samples are written in java and kept very simple. The ideas presented are, however, not limited to java development and should be useful for most of the programming languages on the market. Slides content: I find readable tests a valuable form of documentation in each software project. The slides contain some rules that I believe make the tests more maintainable and easy to understand.
  • 3.
    Hi, I am MarianWamsiedel and I work as a java developer, mostly in java enterprise projects. You can find me at marian.wamsiedel@gmail.com
  • 4.
    What makes aunit test good Strong names or none Declarative Focused Compact Context unaware
  • 5.
  • 6.
    Be focused Test asingle functionality and do it right
  • 7.
    Be focused » Makesure that your test exercises a single piece of functionality » Usually a single assert pro test method will do it. If you write more asserts, make sure they test the same thing » The test method name should be easy to find and contains no „and's“
  • 8.
    Be focused » Avoidpassing boolean flags to a checking method. Write two test methods instead: » private void checkUnderlyingSystemCalled(boolean imported){ if (imported) { verify(oldSystem).calculate(); }else { verify(newSystem).calculate(); } » Not cool: it is difficult to read and violates the single responsability principle
  • 9.
    Be focused » Better:write two methods with clear names @Test public void shouldLoadPortedCustomerHistoryOverLegacySystem(){ Customer ported = customer().ported().build(); underTest.statisticsForCustomer(ported); verify(oldSystem).loadHistory(); }
  • 10.
    Be focused » Better:write two methods with clear names @Test public void shouldLoadRegularCustomerHistoryOverNewSystem(){ Customer regular = customer().regular().build(); underTest.statisticsForCustomer(regular); verify(newSystem).loadHistory(); }
  • 11.
  • 12.
    Be context unaware Donot rely on the external collaborators
  • 13.
    Be context unaware System Havea concentric view of the system: - the unit tests check the functionality of the system - the unit tests fake its collaborators (mock/stub) - the unit tests do not know anything about the external world Collaborators first sec ond External world
  • 14.
    Be context unaware »Never check the functionality of the collaborators » Never call methods on real collaborators, always mock, or stub them » Do not talk to strangers: have absolutely no knowledge about the collaborators of the collaborators
  • 15.
    Be context unaware »There is something wrong if: – Your tests fail to run outside of a suite and each test needs to extend a base test class that provides some setup – You need to mock some global functionality, like: authentication & authorization – Your tests fail if they run in a different order
  • 16.
  • 17.
    Be compact Write shortcode, without duplications
  • 18.
    Be compact » Createonly relevant setup data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 19.
    Be compact, » Eachtest setup block should contain only the setup data that really affects the test result, nothing more » Avoid calling irrelevant "setters", or "allArgsConstructors" » Hide the "noise" information in factory methods, or custom builders create only relevant setup data
  • 20.
    Be compact, » UsingallArgConstructor: Person person = new Person("12343221", "Joe", "Doe", new HomeAddress(...), true, "01.01.1970", true, ...); assertThat( price().for(person), containsDiscount()); //why? » Better: assertThat( price().for(person().withAge(60)), containsDiscount()); create only relevant setup data
  • 21.
    Be compact » Createonly relevant test data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 22.
    Be compact, » Setupdata that affects the test output should be visible. Do not hide relevant information in a helper method, or a factory class » The connection between input data and test output should be clear and should document the production code expose relevant setup data
  • 23.
    Be compact, » assertThat( price().for(person("Joe")), containsDiscount());//why? » assertThat( priceFor(person().withAge(17)), containsDiscount()); //better » assertThat( priceFor(person().withAge(18)), isRegular()); //better expose relevant setup data
  • 24.
    Be compact » Createonly relevant test data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 25.
    Be compact, » Prefercreating test data, over mocking » Mocking the test data make setup methods long and boring to read » Use mocking only to fake the functionality of the system collaborators, if you prefer mocking over stubbing create, don't mock test data
  • 26.
    Be compact, » Mocka teenager customer that lives in London: » Address address = Mockito.mock(Address.class); Customer customer = Mockito.mock(Customer.class); when(address.city()).thenReturn("London"); when(customer.homeAddress()).thenReturn(address); when(customer.age()).thenReturn(14); underTest.doSomethingFor(customer); create, don't mock test data
  • 27.
    Be compact, » Createa teenager user that lives in London: » Customer = customer() .withHomeAddress( address().withCity(„London“)) .withAge(14); underTest.doSomethingFor(customer); create, don't mock test data
  • 28.
    Be compact » Createonly relevant test data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 29.
    Be compact, » Whencreating a test suite, you probably need same, or similar domain objects in different tests » Creating each of them inside each test class where needed leads to duplicated code. Duplication makes maintenance difficult, boring and expensive » There are two approaches that you might consider create test data, options
  • 30.
    Be compact, » Firstapproach: object mother [Fowler], test data warehouse, test data factory » Create a class, that exposes factory methods to create the domain objects the tests need » As the code base grows, it might be difficult to extend and modify the class, since all the tests use the same set of creational methods create test data, object mother
  • 31.
    Be compact, » Secondapproach: let the domain classes expose customized builders, that initialize the required fields with standard data » Let the tests initialize only the relevant fields » This solution should be easier to maintain and extend than the object mother, when the code base grows create test data, custom builders
  • 32.
    Be compact, » CustomerBuilderaCustomer(){ return Customer.builder() .withName("Joe") .withSecondName("Doe") .withAddress(...)} // in the test class » Customer retired = aCustomer().bornIn(1950).build(); assertThat( underTest.priceFor(retired), isDiscounted()); create test data, custom builders
  • 33.
    Be compact » Createonly relevant test data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 34.
    Be compact, » Designthe domain beans as smart classes, that encapsulate data and expose just part of it over its API » The users of the domain bean should not have access to the bean internals » Offer the exposed information to the users, don't let them search for it, repeteadly calling accessor methods smart domain beans, no data bags
  • 35.
    Be compact, » Usejava beans only when required. Resist the temptation to write getters and setters for each private field » Example: a customer can have more addresses: home address, billing address, delivery address and so on. Let's say we need to check the city of the billing address smart domain beans, no data bags
  • 36.
    Be compact, » Thefirst approach, with Customer as java bean // In the test class: » Address billing = customer.getAddresses().stream() .filter(address -> BILLING == address.type()) .findFirst() .orElse(Address.EMPTY); return billing.city(); // The test code gets longer, less readable, duplicated » The test has full knowledge of the customer class internals smart domain beans, no data bags
  • 37.
    Be compact, » Thesecond approach: » class Customer { private List<Address> addresses; public Address billingAddress(){...}; // store the logic here } // In the test class » when(repository.customer(1)).thenReturn( customer().withBillingAddress(address().withCity(„London“)); Customer customer = underTest.customerWithId(1); assertThat( customer.billingAddress().city(), is("London")); smart domain beans, no data bags
  • 38.
    Be compact » Createonly relevant test data » Expose relevant setup data » Create, don't mock test data » Create test data, options » Smart domain beans, no data bags » Minimize code sharing
  • 39.
    Be compact, » Ifyou really need to store some constants in an external file, consider creating a class for each technical area, do not store all test constants in a global class » Consider also a mother object class for each technical area » Do not initialize all mocks in a global setup method. You will end up with tests that rely on initialization made in other test method minimize code sharing
  • 40.
    Be compact, » Usefulquestions: » if you need to refactor the test file, to move it into an external project, how many files would you also need to modify? » if the tests run in a different order, will they still remain green? minimize code sharing
  • 41.
  • 42.
    Be declarative, notimperative Describe what, not how
  • 43.
    Be declarative, notimperative » The natural way to write java code is the imperative one. It describes "how" the job is done and is driven by actions, like: do, set, load, check » The alternative is the declarative style, which describes rather "what" is being done and the result of the actions, hidding the internals of how the action is done
  • 44.
    Be declarative, notimperative » Check that a fresh made espresso is hot, imperative: » goToTheCoffeMachine(); turnItOn(); makeSureItHasEnoughBeans(); … pressTheEspressoButton(); waitUntilTheFinishMessageIsDisplayed(); assertThat( getTheCoffee(), isHot());
  • 45.
    » The declarativeapproach: » Espresso espresso = freshMadeEspresso(); assertThat(underTest, isHot()); » assertThat( havingAFreshMadeEspresso(), isAHotDrink()); Be declarative, not imperative
  • 46.
    Strong names ornone Strong names or none Declarative Focused Compact Context unaware
  • 47.
    Strong names, ornone Make an impact, or be silent
  • 48.
    » Some namesare better than the others: » name the system under test accordingly: - „underTest“, „toTest“ - better than: „form“, „customer“, „mapper“ » generally, the single noun names are easier to read and understand than the long ones: - „repository“, not "shoppingCartRepository" - „from“, not „serviceShoppingCartItem“ - „to“, not „domainShoppingCartItem“ Strong names, or none
  • 49.
    » Avoid storinglocal vars if they don't bring anything new: » Fruit apple = apple(); //nothing new Fruit pear = pear(); Fruit peach = peach(); assertThat( new FruitSalat(apple, pear, peach), containsNo(Strawberry.class)); //inline the vars, if there is no loss of information » assertThat( new FruitSalat(apple(), pear(), peach()), containsNo(Strawberry.class)); Strong names, or none
  • 50.
    » Store localvars if they bring relevant information: » Fruit regularProduct = apple(); Fruit bioProduct = pear(); Fruit localProduct = peach(); FruitSalat mixed = new FruitSalat(regularProduct, bioProduct, localProduct) assertThat( mixed.price(), lowerThan(premiumPrice())); Strong names, or none
  • 51.
    Credits - Growing Object-OrientedSoftware, Guided by Tests (Steve Freeman, Nat Pryce) - Clean Code, A Handbook of Agile Software Craftsmanship (Robert C. Martin) - Elegant Objects (Yegor Bugayenko) - https://martinfowler.com/bliki/ObjectMother.html
  • 52.
    Credits » Presentation templateby SlidesCarnival » Photographs by Unsplash » Photographs by Freepik: – „Designed by Kjpargeter / Freepik“ – „Designed by jcomp / Freepik“ – „Designed by Kjpargeter / Freepik“ – „Designed by Mariia_Fr / Freepik“
  • 53.
    THANKS! You can findme for any questions at marian.wamsiedel@gmail.com Or over linkedin: https://www.linkedin.com/in/marian-wamsiedel-4a1b792b/