Developer Discoveries

TDD: Bottom-up vs Top-down

TDD is an approach used in industry in many ways and useful. In this post, two ways of TDD will be reviewed. First is named as insideout or bottom-up which starts development with lowest level components to highest. Second is outsidein or top-down which starts with highest level components to lowest level. It is difficult to find a good example about TDD. Besides it is difficult to follow from video or article. I think all separate red and green phases can be followed. I want to present two examples for these two approaches. As a testing suite Spring Test is used. There are lots of details of Spring Test, however it is out of this context. I will mention some critical points using Spring Test. I will show two examples in Springboot. Below is the diagram comparing two approaches. Development order is shown with arrows.

topdown-bottomup


TDD: Short Introduction

  • Write test before production code
  • First test must fail (RED)
  • Write simplest code passing the test (GREEN)
  • Refactor
  • Don’t write any test before passing existing ones


Requirements Example

  • User can save a new employee
  • User can update an existing employee
  • User can get an employee by id
  • User can get an employee by name
  • User gets bad request for operations update and get, if given id or name does not exist


First Approach: Bottom-up

When Kent Beck first presented TDD, it was bottom-up. This approach is classissist way. There are no mocks or stubs. Starting with lowest components Red-Green-Refactor cycle runs. I think this is more robust way since developer test highest level components such as controllers with exactly working components not with stubs. Another advantage is that several developers working on same code can reuse existing tests.

As shown below, red lines refer to the version which does not pass test, however green lines refer to the version passing test Having committed all red and green phases in cycles, you can follow the code: https://github.com/gungor/tdd-insideout-example

Cycle 1: Repository: search by name
Cycle 1: Repository: search by name
Cycle 2: Service: save employee
Cycle 2: Service: save employee
Cycle 3: Service: update employee
Cycle 3: Service: update employee
Cycle 4: Service: update employee throws exception when id not exist
Cycle 4: Service: update employee throws exception when id not exist
Cycle 5: Service: get employee
Cycle 5: Service: get employee
Cycle 6: Service: get employee throws exception when id not exist
Cycle 6: Service: get employee throws exception when id not exist
Cycle 7: Service: get employee by name
Cycle 7: Service: get employee by name
Cycle 8: Service: get employee by name throws exception when employee not found by name
Cycle 8: Service: get employee by name throws exception when employee not found by name
Cycle 9: Controller: save employee
Cycle 9: Controller: save employee
Cycle 10: Controller: update employee
Cycle 10: Controller: update employee
Cycle 11: Controller: get employee by id
Cycle 11: Controller: get employee by id
Cycle 12: Controller: get employee by name
Cycle 12: Controller: get employee by name
Cycle 13: Controller: return bad request when employee not found
Cycle 13: Controller: return bad request when employee not found


Second Approach: Top-down
Mockist or outsidein or Top-down approach. It is believed to be started with this paper: Paper at XP
Developer starts writing tests for highest level components first and mock dependent objects. That way there happens little test doubles contrary to classissist way therefore decreases refactoring cost. Another advantage is on the integration side. This approach emphasizes integration of higher level components prior to development of lower components. Thus it allows to design your solution better than classisist way.

As shown below, red lines refer to the version which does not pass test, however green lines refer to the version passing test Having committed all red and green phases in cycles, you can follow the code: https://github.com/gungor/tdd-outsidein-example

Cycle 1: Controller: save employee
Cycle 1: Controller: save employee
Cycle 2: Controller: update employee
Cycle 2: Controller: update employee
Cycle 3: Controller: get employee by id
Cycle 3: Controller: get employee by id
Cycle 4: Controller: get employee by name
Cycle 4: Controller: get employee by name
Cycle 5: Controller: return bad request when employee not found
Cycle 5: Controller: return bad request when employee not found
Cycle 6: Service: save employee
Cycle 6: Service: save employee
Cycle 7: Service: update employee
Cycle 7: Service: update employee
Cycle 8: Service: update employee throws exception when id not exist
Cycle 8: Service: update employee throws exception when id not exist
Cycle 9: Service: get employee
Cycle 9: Service: get employee
Cycle 10: Service: get employee throws exception when id not exist
Cycle 10: Service: get employee throws exception when id not exist
Cycle 11: Service: get employee by name
Cycle 11: Service: get employee by name
Cycle 12: Service: get employee by name throws exception when employee not found by name
Cycle 12: Service: get employee by name throws exception when employee not found by name
Cycle 13: Repository: search by name
Cycle 13: Repository: search by name


Notes on Spring Test

Do not use @Transactional on test methods. It may cause false negatives. There is a good article about that

Use TestEntityManager instead of Repository classes when retrieving records in database in order to assert. Since Hibernate uses first level cache, CrudRepository or JpaRepository may return entities from cache. Since TestEntityManager operations are transactional, it ensures that result received from TestEntityManager fetched from database not from first level cache.

Using TestEntityManager without @DataJpaTest requires transaction. You can use it inside TransactionTemplate.


Notes on TDD

Actually there is not a consensus on TDD. As you know there are classisists and mockists. There are also some authorities who does not think it brings benefit more than it costs. I think TDD can provide benefits in many cases but it costs time. Benefits of using TDD with legacy systems is also questionable. You may think about pros and cons of TDD before starting your project.

This project is maintained by gungor