SlideShare a Scribd company logo
1 of 43
Download to read offline
How to write clean tests
Maksym Danylenko
Agenda
● Unit Tests Naming Convention
● Test structure
● What Makes a Good Test?
● Cleanly Create Test Data
● Keep Cause and Effect Clear
● Don't Put Logic in Tests
● Keep Tests Focused
● Test Behaviors, Not Methods
● Prefer Testing Public APIs Over Private Methods/Implementation-Detail Classes
Unit Tests class Naming Convention
[The name of the tested class]Test
UserServiceRepository
UserServiceRepositoryTest
Integration Tests class Naming Convention
[The name of the tested feature]Test
RegistrationTest
Naming Test Methods If we write tests for a single class
If we write tests for a single class
[the name of the tested method]_[expected input / tested state]_[expected
behavior]
registerNewUserAccount()
registerNewUserAccount_ExistingEmailAddressGiven_ShouldThrowException()
Naming Test Methods If we write tests for a single feature
[the name of the tested feature]_[expected input / tested state]_[expected
behavior]
registerNewUserAccount_ExistingEmailAddressGiven_ShouldShowErrorMessage()
We should find answers to following questions
● What are the features of our application?
● What is the expected behavior of a feature or method when it receives an
input X?
● Also, if a test fails, we have a pretty good idea what is wrong before we read
the source code of the failing test.
What Makes a Good Test?
Clarity - Ясность
Completeness - Полнота
Conciseness - Лаконичность
Resilience - Устойчивость
Cleanly Create Test Data
Test structure
Prepare
Act
Verify
================
Given-When-Then
Test structure
@Test
public void resetPassword_userWithPassword_shouldSetTmpPassword() {
// prepare or given
User user = new User().setPassword("lost password");
//act or when
userService.resetPassword(user);
//verify or then
assertThat(user.getTmpPassword()).isNotEmpty();
}
Could you find expected result?
// Don't
ProductDTO product1 = requestProduct(1);
ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);
Use the Prefixes “actual*” and “expected*”
// Do
ProductDTO actualProduct = requestProduct(1);
ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); // nice and clear.
What Makes a Good Test?
@Test
void calculateTotal_multipleItems_happyPath() {
ShoppingCart shoppingCart = new ShoppingCart(new DefaultRoundingStrategy(),
"unused", NORMAL, false, false, TimeZone.getTimeZone("UTC"), null);
int totalPrice = shoppingCart.calculateTotal(
newItem1(),
newItem2(),
newItem3());
assertThat(totalPrice).isEqualTo(25); // Where did this number come from?
}
Heavily Use Helper Functions:
@Test
void calculateTotal_multipleItems_happyPath() {
ShoppingCart shoppingCart = newShoppingCart();
int totalPrice = shoppingCart.calculateTotal(
newItemWithPrice(10),
newItemWithPrice(10),
newItemWithPrice(5));
assertThat(totalPrice).isEqualTo(25);
}
How easy is it to read this test?
@Test
public void categoryQueryParameter() throws Exception {
List<ProductEntity> products = ImmutableList.of(
new ProductEntity().setId("1").setName("Envelope").setCategory("Office")....,
new ProductEntity().setId("2").setName("Pen").setCategory("Office")....,
new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware")...
);
for (ProductEntity product : products) {
template.execute(createSqlInsertStatement(product));
}
String responseJson = mockMvc.perform(get("/products?category=Office"))
.andExpect(status().is(200))
.andReturn().getResponse().getContentAsString();
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
}
Heavily use helper functions!
@Test
public void getProductsByCategory_shouldReturnFilteredEntities() {
insertIntoDatabase(
createProductWithCategory("1", "Office"),
createProductWithCategory("2", "Office"),
createProductWithCategory("3", "Hardware")
);
String responseJson = mockMvc.perform(get("/products?category=Office"))
.andReturn().getResponse().getContentAsString();
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
}
… but helper methods can grow over time
// This helper method starts with just a single parameter:
Company company = newCompany(Type.PUBLIC);
// But soon it acquires more and more parameters.
Company small = newCompany(2, 2, null, Type.PUBLIC);
Company privatelyOwned = newCompany(null, null, null, Type.PRIVATE);
Company bankrupt = newCompany(null, null, PAST_DATE, Type.PUBLIC);
// Or a new method is added each time a test needs a different combination of //
fields
Company small = newCompanyWithEmployeesAndBoardMembers(2,2, Type.PUBLIC);
Company privatelyOwned = newCompanyWithType(Type.PRIVATE);
Company bankrupt = newCompanyWithBankruptcyDate(PAST_DATE, Type.PUBLIC);
Use the test data builder pattern
Company small = newCompany().employeesCount(2).boardMembersCount(2).build();
Company privatelyOwned = newCompany().type(Type.PRIVATE).build();
Company bankrupt = newCompany().bankruptcyDate(PAST_DATE).build();
Company arbitraryCompany = newCompany().build();
// Zero parameters make this method reusable for different variations of
Company.
private static Company.CompanyBuilder newCompany() {
return Company.builder().type(Type.PUBLIC); // Set required fields
Never rely on default values that are specified by a helper
method
private static Company.CompanyBuilder newCompany() {
return Company.builder().type(Type.PUBLIC); // Set required fields
}
// This test needs a public company, so explicitly set it.
// It also needs a company with no board members, so explicitly clear it.
Company publicNoboardMembers = newCompany()
.type(Type.PUBLIC)
.boardMembersCount(0).build();
Keep Cause and Effect Clear
Can you tell if this test is correct?
207: @Test
208: public void testIncrement_existingKey() {
209: assertThat(counter.get("key1")).isEqualTo(9);
210: }
It’s impossible to say without seeing the whole picture
private final Counter counter = new Counter();
6: @BeforeEach
7: public void setUp() {
8: counter.increment("key1", 8);
9: counter.increment("key2", 100);
10: counter.increment("key1", 0);
11: counter.increment("key1", 1);
12: }
// 200 lines away
210: @Test
210: public void testIncrement_existingKey() {
210: assertThat(counter.get("key1")).isEqualTo(9);
210: }
Write tests where the effects immediately follow the causes
private final Counter counter = new Counter();
@Test
public void increment_newKey_happyPath() {
counter.increment("key2", 100);
assertThat(counter.get("key2")).isEqualTo(100);
}
@Test
public void increment_existingKey_happyPath() {
counter.increment("key1", 8);
counter.increment("key1", 1);
assertThat(counter.get("key1")).isEqualTo(9);
}
Don't Put Logic in Tests
Does this test look correct?
@Test
public void getPhotosPageUrl() {
String baseUrl = "http://photos.google.com/";
UrlBuilder urlBuilder = new UrlBuilder(baseUrl);
String photosPageUrl = urlBuilder.getPhotosPageUrl();
assertThat(photosPageUrl).isEqualTo(baseUrl + "/u/0/photos");
}
What happens if we simplify the test by inlining the
variable?
@Test
public void getPhotosPageUrl_happyPath() {
UrlBuilder urlBuilder = new UrlBuilder("http://photos.google.com/");
String photosPageUrl = urlBuilder.getPhotosPageUrl();
assertThat(photosPageUrl).isEqualTo("http://photos.google.com//u/0/photos");
}
KISS - Keep it stupid simple
KISS - Keep it stupid simple
KISS > DRY (Don’t Repeat Yourself)
KISS > code flexibility
When tests do need their own logic, such logic should often be moved out of the test bodies and into utilities and
helper functions and often with their own tests
Keep Tests Focused
What scenario does the following code test?
@Test
void withdrawFromAccount() {
Transaction transaction = account.deposit(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
assertThat(account.withdraw(usd(1))).isEqualTo(isRejected());
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(1))).isEqualTo(isOk());
}
What scenario does the following code test?
@Test
void withdrawFromAccount() {
Transaction transaction = account.deposit(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
assertThat(account.withdraw(usd(1))).isEqualTo(isRejected());
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(1))).isEqualTo(isOk());
}
It tests three scenarios, not one!
Exercise each scenario in its own test!
@Test
void withdraw_canWithdrawWithinLimits() {
depositAndSettle(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
}
@Test
void withdraw_withdrawOverLimits_shouldBeRejected() {
depositAndSettle(usd(5));
assertThat(account.withdraw(usd(6))).isEqualTo(isRejected());
}
@Test
void withdraw_canOverdrawUpToOverdraftLimit() {
depositAndSettle(usd(5));
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(6))).isEqualTo(isOk());
}
Test Behaviors, Not Methods
Test that verifies an entire method
@Test
public void testResetPassword() {
User user = new User().setPassword("lost password");
userService.resetPassword(user);
assertThat(user.getPassword()).isEmpty();
assertThat(user.getMailbox().getMessages().get(0).getTitle())
.isEqualTo("Password reset");
assertThat(user.getMailbox().getMessages().get(0).getBody())
.startsWith("You have requested password reset");
assertThat(counter.get("reset password")).isEqualTo(1);
}
Separate tests to verify separate behaviors
@Test
public void resetPassword_userWithPassword_usersPasswordShouldBecomeEmpty() {
User user = new User().setPassword("1234");
userService.resetPassword(user);
assertThat(user.getPassword()).isEmpty();
}
@Test
public void resetPassword_userWithPassword_userShouldReceiveEmail() {
User user = new User().setPassword("1234");
userService.resetPassword(user);
assertThat(user.getMailbox().getMessages().get(0).getTitle())
.isEqualTo("Password reset");
assertThat(user.getMailbox().getMessages().get(0).getBody())
.startsWith("You have requested password reset");
}
Only Verify Relevant Method
Arguments
What makes this test fragile?
@Test
public void displayGreeting_showSpecialGreetingOnNewYearsDay() {
clock.setTime(NEW_YEARS_DAY);
user.setName("Frank Sinatra");
userGreeter.displayGreeting();
verify(userPrompter).updatePrompt(
"Hi Frank Sinatra! Happy New Year!",
TitleBar.of("2019-01-01"),
PromptStyle.NORMAL
);
Only verify arguments related to specific behavior being
tested
@Test
public void displayGreeting_showSpecialGreetingOnNewYearsDay2() {
clock.setTime(NEW_YEARS_DAY);
user.setName("Frank Sinatra");
userGreeter.displayGreeting();
verify(userPrompter)
.updatePrompt(eq("Hi Frank Sinatra! Happy New Year!"), any(), any());
}
Arguments ignored in one test can be verified in other tests. Following this pattern allows us to verify only one
behavior per test
Prefer Testing Public APIs
Over
Private
Methods/Implementation-Detail
Classes
Should this classes be tested separately?
class UserInfoValidator {
void validate(UserInfo info) throws ValidationException {
if(info.getDateOfBirth().isInFuture()) {
throw new ValidationException("Invalid date of birth");
}
}
}
public class UserInfoService {
private UserInfoValidator validator;
public void save(UserInfo info) {
validator.validate(info);
writeToDatabase(info);
}
}
Materials
● Modern Best Practices for Testing in Java, link
● Writing Clean Tests – Naming Matters, link
● Testing on the Toilet: Cleanly Create Test Data, link
● Testing on the Toilet: Keep Cause and Effect Clear, link
● Testing on the Toilet: Don't Put Logic in Tests, link
● Testing on the Toilet: Keep Tests Focused, link
● Testing on the Toilet: Test Behaviors, Not Methods, link
● Testing on the Toilet: Prefer Testing Public APIs Over Implementation-Detail
Classes, link
● Testing on the Toilet: Only Verify Relevant Method Arguments, link
Thanks

More Related Content

Similar to How to write clean tests

Working effectively with legacy code
Working effectively with legacy codeWorking effectively with legacy code
Working effectively with legacy code
ShriKant Vashishtha
 

Similar to How to write clean tests (20)

Pragmatic unittestingwithj unit
Pragmatic unittestingwithj unitPragmatic unittestingwithj unit
Pragmatic unittestingwithj unit
 
Best practices unit testing
Best practices unit testing Best practices unit testing
Best practices unit testing
 
A Test of Strength
A Test of StrengthA Test of Strength
A Test of Strength
 
Apex Testing and Best Practices
Apex Testing and Best PracticesApex Testing and Best Practices
Apex Testing and Best Practices
 
"Unit Testing for Mobile App" by Fandy Gotama (OLX Indonesia)
"Unit Testing for Mobile App" by Fandy Gotama  (OLX Indonesia)"Unit Testing for Mobile App" by Fandy Gotama  (OLX Indonesia)
"Unit Testing for Mobile App" by Fandy Gotama (OLX Indonesia)
 
Tech In Asia PDC 2017 - Best practice unit testing in mobile apps
Tech In Asia PDC 2017 - Best practice unit testing in mobile appsTech In Asia PDC 2017 - Best practice unit testing in mobile apps
Tech In Asia PDC 2017 - Best practice unit testing in mobile apps
 
Unit testing with JUnit
Unit testing with JUnitUnit testing with JUnit
Unit testing with JUnit
 
Writing Good Tests
Writing Good TestsWriting Good Tests
Writing Good Tests
 
The secret unit testing tools no one has ever told you about
The secret unit testing tools no one has ever told you aboutThe secret unit testing tools no one has ever told you about
The secret unit testing tools no one has ever told you about
 
Clean tests good tests
Clean tests   good testsClean tests   good tests
Clean tests good tests
 
1 aleksandr gritsevski - attd example using
1   aleksandr gritsevski - attd example using1   aleksandr gritsevski - attd example using
1 aleksandr gritsevski - attd example using
 
An introduction to Google test framework
An introduction to Google test frameworkAn introduction to Google test framework
An introduction to Google test framework
 
Solit 2013, Автоматизация тестирования сложных систем: mixed mode automated t...
Solit 2013, Автоматизация тестирования сложных систем: mixed mode automated t...Solit 2013, Автоматизация тестирования сложных систем: mixed mode automated t...
Solit 2013, Автоматизация тестирования сложных систем: mixed mode automated t...
 
Security Testing
Security TestingSecurity Testing
Security Testing
 
Unit testing patterns for concurrent code
Unit testing patterns for concurrent codeUnit testing patterns for concurrent code
Unit testing patterns for concurrent code
 
Unit testing
Unit testingUnit testing
Unit testing
 
Navigating the xDD Alphabet Soup
Navigating the xDD Alphabet SoupNavigating the xDD Alphabet Soup
Navigating the xDD Alphabet Soup
 
Working effectively with legacy code
Working effectively with legacy codeWorking effectively with legacy code
Working effectively with legacy code
 
Pro Java Fx – Developing Enterprise Applications
Pro Java Fx – Developing Enterprise ApplicationsPro Java Fx – Developing Enterprise Applications
Pro Java Fx – Developing Enterprise Applications
 
Unit testing patterns for concurrent code
Unit testing patterns for concurrent codeUnit testing patterns for concurrent code
Unit testing patterns for concurrent code
 

More from Danylenko Max (6)

Consumer Driven Contract.pdf
Consumer Driven Contract.pdfConsumer Driven Contract.pdf
Consumer Driven Contract.pdf
 
Consumer driven contract
Consumer driven contractConsumer driven contract
Consumer driven contract
 
Fail fast! approach
Fail fast! approachFail fast! approach
Fail fast! approach
 
Monitoring and observability
Monitoring and observabilityMonitoring and observability
Monitoring and observability
 
How to successfully grow a code review culture
How to successfullygrow a code review cultureHow to successfullygrow a code review culture
How to successfully grow a code review culture
 
Testing microservices
Testing microservicesTesting microservices
Testing microservices
 

Recently uploaded

AI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
AI Mastery 201: Elevating Your Workflow with Advanced LLM TechniquesAI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
AI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
VictorSzoltysek
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
mohitmore19
 
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
Health
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
VishalKumarJha10
 

Recently uploaded (20)

A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docx
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
AI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
AI Mastery 201: Elevating Your Workflow with Advanced LLM TechniquesAI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
AI Mastery 201: Elevating Your Workflow with Advanced LLM Techniques
 
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerHow To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
 
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
 
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfLearn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
Introducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) SolutionIntroducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) Solution
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.com
 
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
 
VTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnVTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learn
 
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdfAzure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial Goals
 
How to Choose the Right Laravel Development Partner in New York City_compress...
How to Choose the Right Laravel Development Partner in New York City_compress...How to Choose the Right Laravel Development Partner in New York City_compress...
How to Choose the Right Laravel Development Partner in New York City_compress...
 
Optimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTVOptimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTV
 

How to write clean tests

  • 1. How to write clean tests Maksym Danylenko
  • 2. Agenda ● Unit Tests Naming Convention ● Test structure ● What Makes a Good Test? ● Cleanly Create Test Data ● Keep Cause and Effect Clear ● Don't Put Logic in Tests ● Keep Tests Focused ● Test Behaviors, Not Methods ● Prefer Testing Public APIs Over Private Methods/Implementation-Detail Classes
  • 3. Unit Tests class Naming Convention [The name of the tested class]Test UserServiceRepository UserServiceRepositoryTest
  • 4. Integration Tests class Naming Convention [The name of the tested feature]Test RegistrationTest
  • 5. Naming Test Methods If we write tests for a single class If we write tests for a single class [the name of the tested method]_[expected input / tested state]_[expected behavior] registerNewUserAccount() registerNewUserAccount_ExistingEmailAddressGiven_ShouldThrowException()
  • 6. Naming Test Methods If we write tests for a single feature [the name of the tested feature]_[expected input / tested state]_[expected behavior] registerNewUserAccount_ExistingEmailAddressGiven_ShouldShowErrorMessage()
  • 7. We should find answers to following questions ● What are the features of our application? ● What is the expected behavior of a feature or method when it receives an input X? ● Also, if a test fails, we have a pretty good idea what is wrong before we read the source code of the failing test.
  • 8. What Makes a Good Test? Clarity - Ясность Completeness - Полнота Conciseness - Лаконичность Resilience - Устойчивость
  • 11. Test structure @Test public void resetPassword_userWithPassword_shouldSetTmpPassword() { // prepare or given User user = new User().setPassword("lost password"); //act or when userService.resetPassword(user); //verify or then assertThat(user.getTmpPassword()).isNotEmpty(); }
  • 12. Could you find expected result? // Don't ProductDTO product1 = requestProduct(1); ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED)) assertThat(product1).isEqualTo(product2);
  • 13. Use the Prefixes “actual*” and “expected*” // Do ProductDTO actualProduct = requestProduct(1); ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED)) assertThat(actualProduct).isEqualTo(expectedProduct); // nice and clear.
  • 14. What Makes a Good Test? @Test void calculateTotal_multipleItems_happyPath() { ShoppingCart shoppingCart = new ShoppingCart(new DefaultRoundingStrategy(), "unused", NORMAL, false, false, TimeZone.getTimeZone("UTC"), null); int totalPrice = shoppingCart.calculateTotal( newItem1(), newItem2(), newItem3()); assertThat(totalPrice).isEqualTo(25); // Where did this number come from? }
  • 15. Heavily Use Helper Functions: @Test void calculateTotal_multipleItems_happyPath() { ShoppingCart shoppingCart = newShoppingCart(); int totalPrice = shoppingCart.calculateTotal( newItemWithPrice(10), newItemWithPrice(10), newItemWithPrice(5)); assertThat(totalPrice).isEqualTo(25); }
  • 16. How easy is it to read this test? @Test public void categoryQueryParameter() throws Exception { List<ProductEntity> products = ImmutableList.of( new ProductEntity().setId("1").setName("Envelope").setCategory("Office")...., new ProductEntity().setId("2").setName("Pen").setCategory("Office")...., new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware")... ); for (ProductEntity product : products) { template.execute(createSqlInsertStatement(product)); } String responseJson = mockMvc.perform(get("/products?category=Office")) .andExpect(status().is(200)) .andReturn().getResponse().getContentAsString(); assertThat(toDTOs(responseJson)) .extracting(ProductDTO::getId) .containsOnly("1", "2"); }
  • 17. Heavily use helper functions! @Test public void getProductsByCategory_shouldReturnFilteredEntities() { insertIntoDatabase( createProductWithCategory("1", "Office"), createProductWithCategory("2", "Office"), createProductWithCategory("3", "Hardware") ); String responseJson = mockMvc.perform(get("/products?category=Office")) .andReturn().getResponse().getContentAsString(); assertThat(toDTOs(responseJson)) .extracting(ProductDTO::getId) .containsOnly("1", "2"); }
  • 18. … but helper methods can grow over time // This helper method starts with just a single parameter: Company company = newCompany(Type.PUBLIC); // But soon it acquires more and more parameters. Company small = newCompany(2, 2, null, Type.PUBLIC); Company privatelyOwned = newCompany(null, null, null, Type.PRIVATE); Company bankrupt = newCompany(null, null, PAST_DATE, Type.PUBLIC); // Or a new method is added each time a test needs a different combination of // fields Company small = newCompanyWithEmployeesAndBoardMembers(2,2, Type.PUBLIC); Company privatelyOwned = newCompanyWithType(Type.PRIVATE); Company bankrupt = newCompanyWithBankruptcyDate(PAST_DATE, Type.PUBLIC);
  • 19. Use the test data builder pattern Company small = newCompany().employeesCount(2).boardMembersCount(2).build(); Company privatelyOwned = newCompany().type(Type.PRIVATE).build(); Company bankrupt = newCompany().bankruptcyDate(PAST_DATE).build(); Company arbitraryCompany = newCompany().build(); // Zero parameters make this method reusable for different variations of Company. private static Company.CompanyBuilder newCompany() { return Company.builder().type(Type.PUBLIC); // Set required fields
  • 20. Never rely on default values that are specified by a helper method private static Company.CompanyBuilder newCompany() { return Company.builder().type(Type.PUBLIC); // Set required fields } // This test needs a public company, so explicitly set it. // It also needs a company with no board members, so explicitly clear it. Company publicNoboardMembers = newCompany() .type(Type.PUBLIC) .boardMembersCount(0).build();
  • 21. Keep Cause and Effect Clear
  • 22. Can you tell if this test is correct? 207: @Test 208: public void testIncrement_existingKey() { 209: assertThat(counter.get("key1")).isEqualTo(9); 210: }
  • 23. It’s impossible to say without seeing the whole picture private final Counter counter = new Counter(); 6: @BeforeEach 7: public void setUp() { 8: counter.increment("key1", 8); 9: counter.increment("key2", 100); 10: counter.increment("key1", 0); 11: counter.increment("key1", 1); 12: } // 200 lines away 210: @Test 210: public void testIncrement_existingKey() { 210: assertThat(counter.get("key1")).isEqualTo(9); 210: }
  • 24. Write tests where the effects immediately follow the causes private final Counter counter = new Counter(); @Test public void increment_newKey_happyPath() { counter.increment("key2", 100); assertThat(counter.get("key2")).isEqualTo(100); } @Test public void increment_existingKey_happyPath() { counter.increment("key1", 8); counter.increment("key1", 1); assertThat(counter.get("key1")).isEqualTo(9); }
  • 25. Don't Put Logic in Tests
  • 26. Does this test look correct? @Test public void getPhotosPageUrl() { String baseUrl = "http://photos.google.com/"; UrlBuilder urlBuilder = new UrlBuilder(baseUrl); String photosPageUrl = urlBuilder.getPhotosPageUrl(); assertThat(photosPageUrl).isEqualTo(baseUrl + "/u/0/photos"); }
  • 27. What happens if we simplify the test by inlining the variable? @Test public void getPhotosPageUrl_happyPath() { UrlBuilder urlBuilder = new UrlBuilder("http://photos.google.com/"); String photosPageUrl = urlBuilder.getPhotosPageUrl(); assertThat(photosPageUrl).isEqualTo("http://photos.google.com//u/0/photos"); }
  • 28. KISS - Keep it stupid simple
  • 29. KISS - Keep it stupid simple KISS > DRY (Don’t Repeat Yourself) KISS > code flexibility When tests do need their own logic, such logic should often be moved out of the test bodies and into utilities and helper functions and often with their own tests
  • 31. What scenario does the following code test? @Test void withdrawFromAccount() { Transaction transaction = account.deposit(usd(5)); assertThat(account.withdraw(usd(5))).isEqualTo(isOk()); assertThat(account.withdraw(usd(1))).isEqualTo(isRejected()); account.setOverdraftLimit(usd(1)); assertThat(account.withdraw(usd(1))).isEqualTo(isOk()); }
  • 32. What scenario does the following code test? @Test void withdrawFromAccount() { Transaction transaction = account.deposit(usd(5)); assertThat(account.withdraw(usd(5))).isEqualTo(isOk()); assertThat(account.withdraw(usd(1))).isEqualTo(isRejected()); account.setOverdraftLimit(usd(1)); assertThat(account.withdraw(usd(1))).isEqualTo(isOk()); } It tests three scenarios, not one!
  • 33. Exercise each scenario in its own test! @Test void withdraw_canWithdrawWithinLimits() { depositAndSettle(usd(5)); assertThat(account.withdraw(usd(5))).isEqualTo(isOk()); } @Test void withdraw_withdrawOverLimits_shouldBeRejected() { depositAndSettle(usd(5)); assertThat(account.withdraw(usd(6))).isEqualTo(isRejected()); } @Test void withdraw_canOverdrawUpToOverdraftLimit() { depositAndSettle(usd(5)); account.setOverdraftLimit(usd(1)); assertThat(account.withdraw(usd(6))).isEqualTo(isOk()); }
  • 35. Test that verifies an entire method @Test public void testResetPassword() { User user = new User().setPassword("lost password"); userService.resetPassword(user); assertThat(user.getPassword()).isEmpty(); assertThat(user.getMailbox().getMessages().get(0).getTitle()) .isEqualTo("Password reset"); assertThat(user.getMailbox().getMessages().get(0).getBody()) .startsWith("You have requested password reset"); assertThat(counter.get("reset password")).isEqualTo(1); }
  • 36. Separate tests to verify separate behaviors @Test public void resetPassword_userWithPassword_usersPasswordShouldBecomeEmpty() { User user = new User().setPassword("1234"); userService.resetPassword(user); assertThat(user.getPassword()).isEmpty(); } @Test public void resetPassword_userWithPassword_userShouldReceiveEmail() { User user = new User().setPassword("1234"); userService.resetPassword(user); assertThat(user.getMailbox().getMessages().get(0).getTitle()) .isEqualTo("Password reset"); assertThat(user.getMailbox().getMessages().get(0).getBody()) .startsWith("You have requested password reset"); }
  • 37. Only Verify Relevant Method Arguments
  • 38. What makes this test fragile? @Test public void displayGreeting_showSpecialGreetingOnNewYearsDay() { clock.setTime(NEW_YEARS_DAY); user.setName("Frank Sinatra"); userGreeter.displayGreeting(); verify(userPrompter).updatePrompt( "Hi Frank Sinatra! Happy New Year!", TitleBar.of("2019-01-01"), PromptStyle.NORMAL );
  • 39. Only verify arguments related to specific behavior being tested @Test public void displayGreeting_showSpecialGreetingOnNewYearsDay2() { clock.setTime(NEW_YEARS_DAY); user.setName("Frank Sinatra"); userGreeter.displayGreeting(); verify(userPrompter) .updatePrompt(eq("Hi Frank Sinatra! Happy New Year!"), any(), any()); } Arguments ignored in one test can be verified in other tests. Following this pattern allows us to verify only one behavior per test
  • 40. Prefer Testing Public APIs Over Private Methods/Implementation-Detail Classes
  • 41. Should this classes be tested separately? class UserInfoValidator { void validate(UserInfo info) throws ValidationException { if(info.getDateOfBirth().isInFuture()) { throw new ValidationException("Invalid date of birth"); } } } public class UserInfoService { private UserInfoValidator validator; public void save(UserInfo info) { validator.validate(info); writeToDatabase(info); } }
  • 42. Materials ● Modern Best Practices for Testing in Java, link ● Writing Clean Tests – Naming Matters, link ● Testing on the Toilet: Cleanly Create Test Data, link ● Testing on the Toilet: Keep Cause and Effect Clear, link ● Testing on the Toilet: Don't Put Logic in Tests, link ● Testing on the Toilet: Keep Tests Focused, link ● Testing on the Toilet: Test Behaviors, Not Methods, link ● Testing on the Toilet: Prefer Testing Public APIs Over Implementation-Detail Classes, link ● Testing on the Toilet: Only Verify Relevant Method Arguments, link