Effective Unit & Integration Testing Pitfalls and How to Avoid Them
Atanas Gardev, Petyo Dimitrov 30-05-2024
Slide 2
About us Atanas Gardev Software Architect 20 years’ experience in IT, Java Backend focus 3 kids :)
Petyo Dimitrov Senior Software Architect 17 years’ experience in IT no kids (yet)
Slide 3
Agenda 01 What is a good test?
02 Anti-patterns 03 How to make a better test? 04 AI benefits in testing 05 Q&A 3
Anti-pattern #3 def “user service can create user”() { given: def user = UserMother.johnny() def userForm = new UserForm(user.name, user.address, user.type) when: def result = underTest.createUser(userForm) then: 1 * userRepository.persist(_ as User) >> user 0 * userRepository._ result == user 20
}
Slide 21
Testing implementation vs contract – solution def “user service can create user”() { given: def user = UserMother.johnny() def userForm = new UserForm(user.name, user.address, user.type) userRepository.persist(_ as User) >> user when: def result = underTest.createUser(userForm) then: 0 * userRepository._ result == user 21
}
Slide 22
Anti-pattern #4 – setup @PostMapping @ResponseBody public UserDto create(@RequestBody @Valid UserForm userForm) { User user = userService.create(userForm); return userConverter.convert(user);
}
22
Slide 23
Anti-pattern #4 def “controller calls the service and converter”() { given: def user = UserMother.johnny() def userForm = new UserForm(user.name, user.address, user.type)
when: def result = underTest.create(form) then: 1 * userService.create(form) >> user 1 * userConverter.convert(user) >> … result.name == user.name result.address == user.address
} 23
Slide 24
Don’t write unit tests, write integration tests!
24
Slide 25
Why? • A few Integration/API Tests replace numerous Unit Tests • Easy to develop (e.g. in Spring - @DataJpaTest, @MockBean, MockMvc) • Work with real REST requests / async events • Allow access to the database (where test data may be set up in advance) • 3rd party system calls are handled by mocking the API rather than our client code
25
Slide 26
A refresher
26
Slide 27
Integration test example (1) def “user is created, updated and deactivated after some time”() { given: def user = UserMother.johnny() when: def createdUserDto = /** trigger create REST API */ then: createdUserDto.name == user.name userRepository.findByName(user.name) != null
27
Don’t write integration tests, write unit tests!
29
Slide 30
Source code example def updateState(newState: UserState) { var validStates = [] when (this.state) { Inactive -> validStates = [Active, Deleted] Active -> validStates = [Inactive] } if (validStates.contains(newState)) { state = newState } else { throw IllegalArgumentException() } } 30
Slide 31
Unit test example @Unroll def “updateState should change state from #currentState to #newState”() { given: def user = new User(state: currentState) when: user.updateState(newState) then: user.state == newState where:
31
}
currentState
| newState
UserState.Inactive
| UserState.Active
UserState.Inactive
| UserState.Deleted
UserState.Active
| UserState.Inactive
Slide 32
AI benefits in testing
Slide 33
Popular tools
ChatGPT
33
GitHub Copilot
Slide 34
Explanation 34
Slide 35
Test generation 35
Slide 36
Test data
36
Slide 37
37
Debug
Slide 38
38
Debug
Slide 39
AI considerations • • • • • • 39
Syntax & logical errors Limited edge case coverage Not as creative* Difficulty with complex tests Data Privacy Intellectual Property
Slide 40
ChatGPT config
40
Slide 41
Copilot config
41
Slide 42
Summary • Don’t Repeat Yourself • Avoid mock overuse • Test contracts, not implementation internals • Don’t test for the sake of testing • Distribute your tests across the test pyramid • Let AI help you 42
Slide 43
To me, legacy code is simply code… without tests Michael Feathers Working Effectively With Legacy Code