Effective Unit & Integration Testing - Pitfalls and How to Avoid Them

A presentation at Qinshift Tech Talk in May 2024 in by Petyo Dimitrov

Slide 1

Slide 1

Effective Unit & Integration Testing Pitfalls and How to Avoid Them Atanas Gardev, Petyo Dimitrov 30-05-2024

Slide 2

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

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

Slide 4

Slide 4

What is a good test?

Slide 5

Slide 5

5

Slide 6

Slide 6

Confidence 6

Slide 7

Slide 7

Quick feedback 7

Slide 8

Slide 8

Robust 8

Slide 9

Slide 9

Maintainable 9

Slide 10

Slide 10

Anti-patterns & fixes

Slide 11

Slide 11

Context 11

Slide 12

Slide 12

Spock structure 12 def “test method name”() { given: “prepare” … when: “stimulus” … then: “assertions” … }

Slide 13

Slide 13

Spock syntax def user = Mock(User) user.name >> “Johnny Bravo” user.isDeleted == false 1 * converter.convert(user) 0 * converter._ 13

Slide 14

Slide 14

Anti-pattern #1 def user = Mock(User) user.name >> “Johnny Bravo” user.address >> “Sofia” user.type >> UserType.Corporate user.status >> UserStatus.Active user.isDeleted >> false user.lastLogin >> new Date() 14

Slide 15

Slide 15

Mocking overuse – solution (1) def johnny = new User(“Johnny Bravo”, “Sofia”, UserType.Active) johnny.isDeleted = false johnny.lastLogin = new Date() (2) def johnny = UserMother.johnny() 15

Slide 16

Slide 16

Object Mother pattern UserMother.johnny() UserMother.littleSuzy() AddressMother.sofia() UserMother.johnny() .withAddress( AddressMother.sofia().build()) .build() 16

Slide 17

Slide 17

Anti-pattern #2 def “user conversion works”() { given: def input = new User(“Johnny Bravo”, “Sofia”, Active) when: def result = underTest.convert(input) then: result.name == “Johnny Bravo” result.address == “Sofia” result.type == Active } 17

Slide 18

Slide 18

Repeating yourself – solution def “user conversion works”() { given: def input = UserMother.johnny() when: def result = underTest.convert(input) then: result.name == input.name result.address == input.address result.type == input.type } 18

Slide 19

Slide 19

Utility classes def “user conversion works”() { given: def input = UserMother.johnny() when: def result = underTest.convert(input) then: Utils.matchProperties(result, input, “name”, “address”, “type”, …) } 19

Slide 20

Slide 20

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

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

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

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

Slide 24

Don’t write unit tests, write integration tests! 24

Slide 25

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

Slide 26

A refresher 26

Slide 27

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

Slide 28

Slide 28

Integration test example (2) when: user.lastLogin = now().minusDays(30) def updatedUserDto = /** trigger update REST API / then: updatedUserDto.name == user.name updatedUserDto.lastLogin == user.lastLogin when: jobsUtil.triggerUserPeriodicJob() def userDto = /* trigger read REST API */ then: userDto.name == user.name userDto.status == UserStatus.Deactivated 28

Slide 29

Slide 29

Don’t write integration tests, write unit tests! 29

Slide 30

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

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

Slide 32

AI benefits in testing

Slide 33

Slide 33

Popular tools ChatGPT 33 GitHub Copilot

Slide 34

Slide 34

Explanation 34

Slide 35

Slide 35

Test generation 35

Slide 36

Slide 36

Test data 36

Slide 37

Slide 37

37 Debug

Slide 38

Slide 38

38 Debug

Slide 39

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

Slide 40

ChatGPT config 40

Slide 41

Slide 41

Copilot config 41

Slide 42

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

Slide 43

To me, legacy code is simply code… without tests Michael Feathers Working Effectively With Legacy Code

Slide 44

Slide 44

Questions? 44

Slide 45

Slide 45

Thanks! Atanas Gardev Software Architect atanas.gardev@musala.com Petyo Dimitrov Senior Software Architect petyo.dimitrov@musala.com