A sample codebase accompanying the following article: A guide to safely and efficiently test code that uses repositories.
A shell application is built to allow users to manage their tasks.
A task is a very simple concept here: it's composed of a summary and a description, and it belongs to an owner. This is enough for our demonstration, and it already allows for imagining several realistic use cases.
- A repository interface speaking the language of the domain (i.e. a compile-time contract): TaskRepository
- Automated tests defining the runtime contract that implementations of the above interface must respect: TaskRepositoryContract
- A (so-called) "Fake" in-memory implementation of the repository contract:
InMemoryTaskRepository
(and its tests: InMemoryTaskRepositoryTest).
(Note: a Fake may expose more operations than the contract, for instance here: aclear
method.) - An implementation of the repository contract using PostgreSQL: PostgresqlTaskRepository (and its tests: PostgresqlTaskRepositoryTest)
- A way to easily use Testcontainers with Spring: PostgresqlTest
- The contract tests guarantee that the in-memory repository is as much a valid implementation of the repository as the PostgreSQl one. Those two implementations are interchangeable.
- Fixtures for easily writing unit tests using of the in-memory implementation of the contract: TaskFixtures, TaskCommandsFixtures
- No test uses "Mocks", neither manually written nor generated by a library. Instead, tests use the actual implementations of all classes, except that in-memory repositories are used. (But as written earlier, in-memory repositories are fully valid implementations.). See TaskMergeServiceTest, CurrentUserTaskServiceTest or TaskCommandsTest for examples of unit tests making use of it, both in the domain module and in the app module.
While not related to our main demonstration, the code also features:
- The use of Liquibase to evolve our DB schema. See db.changelog-master.yaml, task-00002-create-task-table.sql, etc.
- An example of the Specification pattern: TaskSpecifications and a way to map such specifications to SQL queries.
- The extraction of common test fixtures in a single place to make tests more maintainable: TaskFixtures and TaskCommandsFixtures
- A builder to easily create test tasks: TaskBuilder
Because:
- for this example, it doesn't matter which kind of application is built
- we work with Web apps every day :-)
Unfortunately the shell doesn't play well with Spring's bootRun
task, that's why the app must be
built first:
# build the app
./gradle build
# start the DB
docker-compose -f dev-env/docker-compose.yml start
# start the app
java -jar task-shell-app/build/libs/task-shell-app-0.0.1-SNAPSHOT.jar
# play a bit with the app, type "quit" to quit
# stop the DB
docker-compose -f dev-env/docker-compose.yml stop