The foundation of your test suite will be made up of unit tests. Your unit tests make sure that a certain unit (your subject under test) of your codebase works as intended. Unit tests have the narrowest scope of all the tests in your test suite. The number of unit tests in your test suite will largely outnumber any other type of test.
https://martinfowler.com/articles/practical-test-pyramid.html#UnitTests
Pros | Cons |
---|---|
Fast | Unit test alone doesn't provide guarantees about the behavior of the system |
Stable and reliable | |
Independent and isolate failures | |
Easy to write and maintenance |
Boundary
Base on our current architecture, we have all of our business logic inside Service Layer.
So the boundary of Unit Test is the classes inside service layer.
Where to put the unit tests
under the folder /test
Objectives
protection against regressions
resistance to refactoring
fast feedback
maintainability
Guidelines
Do not ignore/comment out the failing tests
fix it if some business logic get broken
delete it if the test is not required anymore
Test the smallest unit of code, each test case cover one business logic(condition or branch)
Must have Strong assertions, avoid assertion-free testing
Use test doubles to mock dependencies (recommend to use https://mockk.io/#annotations )
Keep unit test simple and straightforward
No try catch (use
assertThrows
to assert method which throws exception)No if else
No loops (use
ParameterizedTest
)
Try to re-use test data How to define a Fixture
Must have good code coverage (90% above), check test coverage report(
build/jacocoReport/index.html
) if you are not confident with code coverage
How to Write Unit Test
Create Test Class
One test class per production class
Test class should have same package name with production class but under test folder
Test class should be named
XXXXTest
(XXXX
is the production class under testing)Create test class for implementation instead of interface
Press Command + Shift + T
in the class file under testing, click ok
to create test file
Inject Mock Dependencies
Mock all dependencies, prefer to use https://mockk.io/#annotations
Prefer to annotate
@ExtendWith(MockKExtension::class)
on unit test (@MicronautTest
starts a micronaut server which make test 10x slower than pure unit test)
@ExtendWith(MockKExtension::class) class CustomerSupportServiceImplTest { @InjectMockKs private lateinit var customerSupportService: CustomerSupportServiceImpl @MockK private lateinit var customerManagerApiClient: CustomerManagerApiClient @MockK private lateinit var auditLogProducer: AuditLogProducer }
Test Business Logic
Follow a basic pattern “Given, When, Then“ https://martinfowler.com/bliki/GivenWhenThen.html
Test the public method of the class
Use a sentence to describe what behavior is under testing ( for example
method name - should return null with more than one customer found
) and test one condition per test. This helps you to keep your tests short and easy to understand.Test should be simple and straightforward
No try catch (use
assertThrows
to assert method which throws exception)No if else
No loops (use
ParameterizedTest
)
Put test data in
companion object
at end of test file, or create aFixture
file to put the data sharing in multiple tests
@Test fun `find customer - should return the customer`() { every { customerManagerApiClient.findCustomer(phoneNumber.countryCode, phoneNumber.number, true) } returns customerDtoResponse val customer = customerSupportService.findCustomer(customerFullPhoneNumber) assertTrue(customer?.phoneNumber?.countryCode == phoneNumber.countryCode) assertTrue(customer?.phoneNumber?.phoneNumber == phoneNumber.number) verify(exactly = 1) { customerManagerApiClient.findCustomer(phoneNumber.countryCode, phoneNumber.number, true) auditLogProducer.send(null, customerDtoResponse.results[0].customer.id, any()) } } companion object { private val ACCOUNT_ID = UUID.randomUUID() private val phoneNumber = CustomerDtoPhoneNumber("12", "0123456789") private val customerFullPhoneNumber = phoneNumber.countryCode + phoneNumber.number private val customer = CustomerDto(ACCOUNT_ID, true, null, null, null).phoneNumber(phoneNumber) private val customerDtoResponse = CustomerSearchResultDto(listOf(CustomerSnapshotDto(customer)), 1, 0, 10) private val emptyCustomerDtoResponse = CustomerSearchResultDto(emptyList(), 0, 0, 10) private val multipleCustomersDtoResponse = CustomerSearchResultDto(listOf(CustomerSnapshotDto(customer), CustomerSnapshotDto(customer)), 2, 0, 10) }
Understand Test Coverage
Test coverage report can help us to identify uncovered/untested business logic.
From the below coverage file, there are some branches not fully covered line 49 highlighted with red color
and line 26, 42, 43 highlighted with yellow color
, that’s why coverage report show 88% of line coverage and 75% branch coverage.
In this case you need to add test case to covered these branches.
Refactor
Once we have all business logic covered by unit test, we are confident to refactor our code without concerns of breaking any features.
Attachments:
Screen Shot 2022-11-15 at 17.15.52.png (image/png)
Screen Shot 2022-11-15 at 17.15.52.png (image/png)
Screen Shot 2022-11-15 at 17.05.46.png (image/png)
Screen Shot 2022-11-14 at 17.07.57.png (image/png)
image-20221111-082148.png (image/png)
Screen Shot 2022-11-14 at 17.08.08.png (image/png)
Screen Shot 2022-11-14 at 17.06.12.png (image/png)
Blank diagram - Page 1.png (image/png)
Screen Shot 2022-11-14 at 01.20.30.png (image/png)
Screen Shot 2022-11-14 at 13.58.26.png (image/png)
Screen Shot 2022-11-14 at 13.38.00.png (image/png)
Blank diagram - Page 1 (3).png (image/png)
Blank diagram - Page 1 (2).png (image/png)