/springboot-tdd

(ARCHIVED) TDD workflow for SpringBoot backend

Primary LanguageJava

SpringBoot TDD

GitHub last commit
GitHub top language Lines of code GitHub code size in bytes

Implementations

  • feature-based folder
  • formatter: eclipse-java-google-style
  • checkstyle: IDE + google_checkstyle
  • linting: sonarlint
  • coverage: run mvn jacoco:prepare-agent test install jacoco:report
  • dockerized mysql for tests
  • dev/prod separation deployments
  • business logic in services only
  • explicit mapping dto:entity (mapstruct yields unreachable code)
  • validation
  • controller-based exception handling
  • ci + coverage stickers

Best Practices

Code:

  • Only services are imported into RestControllers. No logic, validation or repositories involved.
  • Do not write validations by hand. Use spring-boot-starter-validation instead
  • Only Validate DTO's. This saves network resources if data not in proper format
  • Request/Response returned should only be DTO's (avoid 1:1 mapping of db entities and json)
    Use explicit custom/generic mapping for request/response.
    The goal is to avoid sending very large models and reduce API calls
  • Avoid @Autowired and use @RequiredArgsConstructor and private final
    This provides more room for unit testing.
  • Take note when @Data or @ToString on @Entity with any relationship like @OneToMany
    Make sure to include @ToString.Exclude on that specific field to fix conflicts
  • Create a lombig.config file on the root directory with contents
    config.stopBubbling = true and lombok.addLombokGeneratedAnnotation = true to include @lombok.Generated annotations for test coverage in jacoco
  • Services should contain all business logic
  • DTO/Entity conversions should happen in Services
  • Repositories should contain all predefined/custom DB logic
  • Unless you find a way to progmatically run checkstyles and sonarlint,
    make sure push code with no warnings from ide lints.
  • Use @ControllerAdvice for Global Exception handler i.e. malformed requests, invalid path etc etc
    otherwise use @ExceptionHandlers per feature controller
  • Use easy-random for fast creation of pojo's in tests

Tests:

  • Only use @Mock on unit-tested components
  • Unit tests should not use Spring: no SpringBootTest, MockBean and Autowired
  • All classes should only use @RequiredArgsConstructor and private final fields
  • Use @ExtendWith(MockitoExtension.class) to enable @Mock annotation.
  • Use InjectMocks for components requiring mocked
  • Validations and logic for incoming requests should be examined at unit-tests
  • Mid unit-integration-test types should @Mock all external dependencies
  • Only test atleast one success/failure case for incoming requests on Controllers.
  • Controllers: MockMvc for unit-tests, WebTestClient for integration-test
  • Check serialization for each json response type of Controllers
  • Mocks only for unit-tests (e.g. services, logic, validations)
  • Test slices for mid unit-integration-tests (e.g. controllers, jpa)
  • SpringBootTest for integration-tests (e.g. testcontainers, external services)
  • Integration Tests should reflect overall behaviour of grouped components
  • Repositories are only tested on integration-tests since JPA does the heavy lifting.
    Testcontainers are more suited to reflect the production database, hence integration-test.
  • End-to-end tests should represent pre-production environment
  • Always prefer to use JUnit5. @Test annotation should point to org.junit.jupiter.api.Test

Running Tests

  • Use mvn clean install > log-file.log in CI/CD
    Putting all logs into a logfile is necessary to avoid VM crashes.
  • Use IDE test runs when developing locally

integration-tests:

  • It talks to the database
  • It communicates across the network
  • It touches the file system
  • It can’t run at the same time as any of your other unit tests
  • You have to do special things to your environment (such as editing config files) to run it

paths

  • GET /users/{username}
  • GET /users
  • POST /users
  • PUT /users
  • DELETE /users/{username}

user

  • id
  • username
  • bio

notes

  • put "coverage-gutters.coverageReportFileName": "target/site/jacoco/index.html"
    in vscode settings.json when developing in Java to enable coverage gutters

lombok notice

  • exclude id fields from hashcode/equals
  • exclude association fields which are not managed in given entity from hashcode/equals
  • exclude all lazy loaded fields from toString method
  • exclude fields possibly causing circular references from toString method

useful guides

stickers (reference)