Skip to content

Latest commit

 

History

History
213 lines (170 loc) · 8.82 KB

testing.md

File metadata and controls

213 lines (170 loc) · 8.82 KB

Testing

codecov

codecov

There are 5 different types of tests included in this project. I also achieved %100 coverage by writing tests for everything except configuration classes.

Unit Tests
Database Tests
Controller Tests
Contract Tests
Postman Tests

Unit Tests

Unit tests are very easy to write, and they run very fast compared to other tests. They can be used to test business logic in the domain part of the application.

You can also use unit tests if you don't need to test the actual behavior of a bean by mocking it. To do this, you can mock the bean you want by using Mockito in your test class. Inspect BaseUnitTest and this example test for more information.

Database Tests

You may want to test the actual behavior of your persistence layer when working with databases. In that case unit tests won't be enough. You can set up a database test configuration by using Testcontainers, which provides an efficient way to test databases.

  1. Add Testcontainers dependency for your database in your build.gradle file. Since I am using postgres in this project, I added the testcontainers:postgresql dependency.
  2. Create a BaseDatabaseTest class to setup testcontainers and common configurations.
  3. (Optional) Create a helper class that can persist and find entities. This way you don't have to persist an entity with a class you are trying to test. Make the BaseDatabaseTest extend the helper class.
  4. You can start testing by extending BaseDatabaseTest in your test class .

Controller Tests

You can test the controller layer of your application by using MockMvc. The main goal is to test whether your controller is directing your request to the correct destination.

  1. Create a BaseControllerTest class to setup common configurations. I also imported a TestSecurityFilterConfig in that class to do two things:
    • I didn't include the OAuth2 part and disabled csrf (unlike in the actual security filter) since I'm not testing them. Please note that it's okay to disable csrf for testing purposes, but it should always be enabled in production.
    • I added @EnableWebSecurity and @EnableMethodSecurity just like in the actual security filter, so that the authorization part can also be tested.
  2. (Optional) Create user specific annotations to mimic users with specific roles. This is just for code reuse and makes your tests easier to read and write.
  3. You can start testing by extending BaseControllerTest in your test class.

Contract Tests

When a microservice sends a rest request to another microservice, it's a good practice to test that both ends are synchronized. There are a couple of ways to do it. For example, it can be tested by running the entire application stack, but it could be very costly. On the other hand unit tests are very cost-efficient, but they can't really test the actual behavior.

The most efficient and reliable way to test the synchronous communication between services is contract tests. In this project I implemented consumer driven contract tests. It's based on sharing the contract generated by the producer microservice, with the consumer microservice. Since both producer and consumer depend on the same contract, they will stay synchronized.

I'll explain how to write a contract test with an example in this project. In an order's detail, there is also a payment information. The order microservice sends a request to payment microservice to fetch that payment information. So in this scenario, order microservice is the consumer and payment microservice is the producer.

Testing the Producer

  1. Add the necessary dependencies in your build.gradle file.
// build.gradle
plugins {
   id 'org.springframework.cloud.contract' version '<version>'
}

dependencies {
   testImplementation "org.springframework.cloud:spring-cloud-starter-contract-verifier:<version>"
}
  1. For contract tests to use JUnit, add this code block.
// build.gradle
contractTest {
   useJUnitPlatform()
}
  1. Specify where Spring Cloud Contract will look for base contract tests and where the contracts are located.
// build.gradle
contracts {
   contractsDslDir = file("src/test/resources/contracts")
   packageWithBaseClasses = 'com.solidvessel.payment.adapter.in'
   baseClassMappings {
      baseClassMapping("payment", "com.solidvessel.payment.adapter.in.payment.rest.PaymentProducerContractTest")
   }
}
  1. Create a BaseProducerContractTest class. This is only for specifying the Spring profile the tests will run.
  2. Create your contract. In this file, you need to specify the actual response you require.
  3. Write your base test class for your scenario.
  4. Generate the tests by running ./gradlew build. This will compare your controller's response with the response you specified in the groovy file. If it's correct, a stub will be generated to share with the consumer.

If you modify the producer and there is a breaking change, your test will fail, and you will need to update your contract test. By doing that the updated version of the stub will be ready to share with the consumer.

Testing the Consumer

  1. Add the necessary dependencies in your build.gradle file.
// build.gradle
dependencies {
   testImplementation "org.springframework.cloud:spring-cloud-contract-stub-runner:<version>"
}
  1. Inject the stub generated by the producer.
// build.gradle
dependencies {
   testImplementation files(
           "${project.rootDir}/payment/infra/build/libs/infra-stubs.jar"
   )
}

If you have a multi repo project, you need to do this with a remote repository url.

  1. Make sure that your consumer's copying contracts won't start before your producer generates stubs, otherwise there'll be a compilation error.
// build.gradle
tasks.copyContracts.dependsOn rootProject.tasks.findByPath('payment:infra:verifierStubsJar')

Considerations

Question: Doesn't this create a dependency?

Answer: Yes, so do sending a rest request to a microservice. Prefer a less dependent approach as much as possible, such as events.

Question: What if there is a dependency cycle? What if 2 microservices are both producer and consumers of each other?

Answer: It should be still possible to arrange this, but it would be extremely difficult to test and such a cycle shouldn't be there at the first place.

  1. Create a BaseConsumerContractTest class. In this class I enabled Feign clients to send rest requests, and disabled every other bean. Also, I specified the url I'll send the request in the application-contracttest.yml file.
  2. Write your consumer contract test. The @AutoConfigureStubRunner annotation will get the stub generated by the producer and start a mock server, so you can send an actual request.

If your test fails, that means you are not up-to-date with the producer, and you need to update your code.

Postman Tests

There are also Postman tests to test the application. You can import the Postman collection and environments in .postman folder, run the application and try it out yourself.