This is the test module of spring-boot-crud. It brings some test utilities and base classes for testing your CRUD applications built with spring-boot-crud-api. See Contents section for details of each class it provides.
To get started, add spring-boot-crud-test as a test dependency to your project.
For Gradle with Kotlin DSL, add following to build.gradle.kts
:
dependencies {
testImplementation('dev.akif:spring-boot-crud-test:0.5.1')
}
For Gradle, add following to build.gradle
:
dependencies {
testImplementation 'dev.akif:spring-boot-crud-test:0.5.1'
}
For Maven, add following to your pom.xml
:
<dependencies>
<dependency>
<groupId>dev.akif</groupId>
<artifactId>spring-boot-crud-test</artifactId>
<version>0.5.1</version>
<scope>test</scope>
</dependency>
</dependencies>
There are two main things spring-boot-crud-test provides: a test environment with utilities to be used in tests and base test classes which give you test cases for free, the same way spring-boot-crud-api gives you an API for free.
Setting up a proper testing environment can be hard. In tests, especially in unit tests, you'll need mock/dummy/custom implementations of certain components. spring-boot-crud-test tries to make it easier for you to write tests for your applications by providing a few of those.
For the remainder of this document, we'll follow the example from spring-boot-crud-api documentation, so we'll add tests to our cats API.
There is an InstantProvider component used for getting the current java.time.Instant
. AdjustableInstantProvider
is a custom implementation of this that can be used in tests. It provides:
- A constant time so every time
now
is called, the sameInstant
is returned - An
adjust
method to modify the time so the passage of time can be simulated in tests - A
reset
method to reset the modifications made to the time
This is a map-based in-memory implementation of CRUDRepository to use in tests. It implements the repository methods the same way a real database. It supports having initial entities and clean
/reset
for specific test cases. It can be implemented as:
import dev.akif.crud.InMemoryCRUDRepository
import java.util.UUID
object InMemoryCatRepository: InMemoryCRUDRepository<UUID, CatEntity, CreateCat, CatTestData>(CatTestData), CatRepository
Please note:
- This also extends
CatRepository
so it can be injected toCatService
as a repository dependency.
1.3. IdGenerator
This is a simple interface for generating ids. It is used in InMemoryCRUDRepository
to generate ids for new entities. It is also used in CRUDTestData
to generate random ids.
1.4. CRUDTestData
This lets you organize your test data and gives you some test utilities. Things to highlight are:
- Gives you access to test utilities.
- You're required to have 3 different instances of your entity as test data (namely
testEntity1
,testEntity2
andtestEntity3
). This is to have a minimal setup allowing pagination. You can always define more by setting themoreTestEntities
array. - Since there are entities and entities are mutable by nature, there is a
copy
method so a copy of an entity can be created. This is useful in creating expected cases in assertions. - There is an
areDuplicates
method for defining the uniqueness condition of your entities. - There are
xToY
methods for mapping between different versions of an entity (with or without some modifications). This is useful when testing multiple layers.
A test data class for cats could be implemented as:
import dev.akif.crud.CRUDTestData
import java.util.UUID
import org.springframework.data.domain.PageRequest
object CatTestData : CRUDTestData<UUID, CatEntity, Cat, CreateCat, UpdateCat, CatTestData>(typeName = "Cat") {
override val repository: InMemoryCRUDRepository<UUID, CatEntity, CreateCat, CatTestData>
get() = InMemoryCatRepository
override val idGenerator: IdGenerator<UUID> =
IdGenerator.uuid
private val catId1 = idGenerator.next()
private val catId2 = idGenerator.next()
private val catId3 = idGenerator.next()
override val testEntity1: CatEntity =
CatEntity(
id = catId1,
name = "Cookie",
breed = "Tabby",
age = 4,
version = 0,
createdAt = now(),
updatedAt = now(),
deletedAt = null
)
override val testEntity2: CatEntity =
CatEntity(
id = catId2,
name = "Kitty",
breed = "Persian",
age = 3,
version = 0,
createdAt = now().plusSeconds(1),
updatedAt = now().plusSeconds(1),
deletedAt = null
)
override val testEntity3: CatEntity =
CatEntity(
id = catId3,
name = "Meowth",
breed = "Scottish Fold",
age = 2,
version = 0,
createdAt = now().plusSeconds(2),
updatedAt = now().plusSeconds(2),
deletedAt = null
)
override val defaultFirstPageEntities: List<CatEntity> =
listOf(
testEntity1,
testEntity2,
testEntity3
)
override val paginationTestCases: List<Pair<PageRequest, Paged<CatEntity>>> =
listOf(
PageRequest.of(0, 2) to Paged(
data = listOf(testEntity1, testEntity2),
page = 0,
perPage = 2,
totalPages = 2
),
PageRequest.of(1, 2) to Paged(
data = listOf(testEntity3),
page = 1,
perPage = 2,
totalPages = 2
),
PageRequest.of(2, 2) to Paged.empty(page = 2, perPage = 2, totalPages = 2)
)
override val moreTestEntities: Array<CatEntity> =
emptyArray()
override val testParameters: Parameters =
Parameters.empty
override fun areDuplicates(e1: CatEntity, e2: CatEntity): Boolean =
e1.name == e2.name
&& e1.breed == e2.breed
&& e1.age == e2.age
override fun copy(entity: CatEntity): CatEntity =
CatEntity(
id = entity.id,
name = entity.name,
breed = entity.breed,
age = entity.age,
version = entity.version,
createdAt = entity.createdAt,
updatedAt = entity.updatedAt,
deletedAt = entity.deletedAt
)
override fun entityToCreateModel(entity: CatEntity): CreateCat =
CreateCat(
name = entity.name!!,
breed = entity.breed!!,
age = entity.age!!
)
override fun entityToUpdateModelWithModifications(entity: CatEntity): UpdateCat =
UpdateCat(
name = "${entity.name}-updated",
age = entity.age?.plus(1) ?: 1
)
override fun entityToUpdateModelWithNoModifications(entity: CatEntity): UpdateCat =
UpdateCat(
name = entity.name!!,
age = entity.age!!
)
}
Unit tests are fundamental in software testing. While writing unit tests, we focus on a single unit. In a CRUD application, this would entail different components at different layers. spring-boot-crud-test provides base classes for such tests.
2.1. CRUDServiceTest
This is a base class for the unit test of a service class. It provides the required set-up and following test cases for free:
- creating new entities
- should fail with already exists error when trying to create an entity that already exists
- should create a new entity with the same data of a deleted entity and return it
- should create a new entity and return it
- getting all entities
- should return at least 3 entities with default pagination
- should not return deleted entities
- should return correct entities with pagination
- should return empty page when no entities exist
- getting an entity
- should return null when trying to get a deleted entity
- should return correct entity
- should return null when trying to get an entity that doesn't exist
- updating an entity
- should fail with already exists error when trying to update an entity as duplicate of another entity
- should fail with not found error when trying to update an entity that doesn't exist
- should update return updated entity
- should update with the same data of a deleted entity return updated entity
- deleting an entity
- should fail with not found error when trying to delete an entity that doesn't exist
- should delete
A unit test for CatService
could be implemented as:
import dev.akif.crud.CRUDServiceTest
import org.junit.jupiter.api.DisplayName
import java.util.UUID
@DisplayName("CatService")
class CatServiceTest : CRUDServiceTest<UUID, CatEntity, Cat, CreateCat, UpdateCat, CatMapper, CatRepository, CatService, CatTestData>(
mapper = CatMapper(ToyMapper()),
testData = CatTestData,
buildService = { mapper, testData -> CatService(testData.instantProvider, InMemoryCatRepository, mapper) }
)
Please note that you can use given dependencies mapper
and testData
to build CatService
. These are the dependencies CRUDServiceTest
builds for you internally, which are reset for every test case.
Test will also get more complicated as the problem domain grows. You can find a complete Cat API that supports toys in kotlin-spring-boot-template repository.