Skip to content

This project provides a framework for structuring unit tests following the AAA (Arrange, Act, Assert) pattern with a focus on clarity and readability.

License

Notifications You must be signed in to change notification settings

co-mmer/aaa-mockmvc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aaa-mockmvc-icon

AAA-MockMvc

Quality Gate Status Coverage SQALE Rating Security Rating Vulnerabilities Code Smells Java Maven Central

Overview

This project provides a framework for structuring unit tests following the AAA (Arrange, Act, Assert) pattern with a focus on clarity and readability. The library offers a fluent API that guides the developer through each phase, ensuring that only contextually relevant methods are available at each step. This design choice enhances the ease of writing, maintaining, and understanding tests. The key goal is to provide a clean separation of concerns within the test, allowing developers to express their test logic fluently and naturally.


Table of Contents


Installation

To include AAAMockMvc in the project, add the following dependency to the pom.xml. The sources can also be downloaded directly within the IDE (e.g., IntelliJ IDEA) to access the documentation of the classes.

<dependency>
  <groupId>io.github.co-mmer</groupId>
  <artifactId>aaa-mockmvc</artifactId>
  <version>1.4.0</version>
  <scope>test</scope>
</dependency>

Setup

To write tests using this library, certain configurations are necessary. Below are the steps required to set up your testing environment effectively.

Bean

To configure AAAMockMvc for tests, four main options are available. Each configuration offers flexibility in using AAAMockMvc within the test setup to interact with the MVC testing framework, tailored to the specific project requirements.

Setup A: WebApplicationContext with Default ObjectMapper

In this option, AAAMockMvc is configured using the WebApplicationContext. The framework will use a default ObjectMapper (new ObjectMapper()).

Steps:

  1. Define a configuration class.
  2. Create a AAAMockMvc bean using the WebApplicationContext.
@Configuration
public class AAAMockMvcConfig {

  @Bean
  public AAAMockMvc aaaMockMvc(WebApplicationContext context) {
    return new AAAMockMvc(context);
  }
}
Setup B: WebApplicationContext with Custom ObjectMapper

In this option, both the WebApplicationContext and a custom ObjectMapper can be passed to AAAMockMvc.

Steps:

  1. Define a configuration class.
  2. Create a ObjectMapper bean with custom configuration.
  3. Pass the custom ObjectMapper instance to the AAAMockMvc bean
@Configuration
public class AAAMockMvcConfig {

  @Bean
  AAAMockMvc aaaMockMvc(WebApplicationContext context, ObjectMapper objectMapper) {
    return new AAAMockMvc(context, objectMapper);
  }

  @Bean
  public ObjectMapper objectMapper() {
    // Custom ObjectMapper configuration
    return new ObjectMapper();
  }
}
Setup C: Custom MockMvc and Default ObjectMapper

This option allows for configuring AAAMockMvc with a custom MockMvc instance. The framework will use a default ObjectMapper (new ObjectMapper()).

Steps:

  1. Define a configuration class.
  2. Create a MockMvc bean with custom configuration.
  3. Pass the custom MockMvc instance to the AAAMockMvc bean
@Configuration
public class AAAMockMvcConfig {

  @Bean
  AAAMockMvc aaaMockMvc(MockMvc mockMvc) {
    return new AAAMockMvc(mockMvc);
  }

  @Bean
  public MockMvc mockMvc(WebApplicationContext context) {
    // Custom MockMvc configuration
    return MockMvcBuilders.webAppContextSetup(context)
        .addFilters(new CharacterEncodingFilter("UTF-8", true))
        .build();
  }
}
Setup D: Custom MockMvc and Custom ObjectMapper

This option allows for full customization by passing both a custom MockMvc instance and a custom ObjectMapper to AAAMockMvc. This provides maximum flexibility for projects that need specific configurations.

Steps:

  1. Define a configuration class.
  2. Create a MockMvc bean with custom configuration.
  3. Create a ObjectMapper bean with custom configuration.
  4. Pass the custom MockMvc and ObjectMapper instance to the AAAMockMvc bean
@Configuration
public class AAAMockMvcConfig {

  @Bean
  AAAMockMvc aaaMockMvc(WebApplicationContext context, ObjectMapper objectMapper) {
    return new AAAMockMvc(context, objectMapper);
  }

  @Bean
  public MockMvc mockMvc(WebApplicationContext context) {
    // Custom MockMvc configuration
    return MockMvcBuilders.webAppContextSetup(context)
        .addFilters(new CharacterEncodingFilter("UTF-8", true))
        .build();
  }

  @Bean
  public ObjectMapper objectMapper() {
    // Custom MockMvc configuration
    return new ObjectMapper();
  }
}

Test

There are two options for utilizing the AAAMockMvc in test classes:

Setup A: AAAMockMvc

AAAMockMvc can be directly autowired into the test class. This method allows the API methods to be used directly in the tests.

public class ControllerTest {

  @Autowired
  private AAAMockMvc aaaMockMvc;

  @Test
  void WHEN_calling_endpoint_THEN_return_expected_status() {
    aaaMockMvc.get()
        .arrange()
        .arrangeUrl(GET_SIMPLE)
        .act()
        .actPerform()
        .asserts()
        .assertStatus()
        .assertStatusIsOk();
  }
}
Setup B: AAAMockMvcAbstract

Alternatively, extending the abstract class AAAMockMvcAbstract is another option, which provides all necessary methods like get(), post(), etc. This approach is useful for encapsulating common test behaviors and reducing boilerplate code in test classes.

public class ControllerTest extends AAAMockMvcAbstract {

  @Test
  void WHEN_calling_endpoint_THEN_return_expected_content() {
    get()
        .arrange()
        .arrangeUrl(GET_SIMPLE)
        .act()
        .actPerform()
        .asserts()
        .assertStatus()
        .assertStatusIsOk();
  }
}

Writing Tests

In the provided library, every test follows the AAA structure using the following three phases:

  1. Arrange: Set up the necessary conditions for the test (e.g., define URL, parameters, headers).
  2. Act: Perform the operation (e.g., make the API request).
  3. Assert: Validate the result (e.g., check HTTP status, response content).
  4. Answer: Optionally, the result of the request can be accessed using the answer() method, allowing for further examination and validation of the response. Additionally, the returned object can be passed back as the body of a subsequent request using arrangeJson(T content). This enables used to set up the next, creating a seamless flow in testing. For more details, see the example below.

Arrange

Assert

Answer


Example

This example demonstrates a typical registration process, where the user's email address must first be verified by sending a code. After the email verification, the user can proceed with the registration, including providing the verification code.

The process is split into two main steps:

  1. Email Verification: A verification code is sent to the provided email address.
  2. Registration: After receiving the verification code, the user sends it back to the backend for validation, allowing them to complete their registration.

Below are the necessary code snippets to represent this scenario:

Registration Scenario

1. Verification Response Model

The VerificationResponse class represents the response that contains the verification code, UUID, and email after the verification process:

public class VerificationResponse {

  private String code;
  private String uuid;
  private String mail;
}

2. Verification Controller

The VerificationController is responsible for handling the verification request. It receives a request with the user's email and generates a verification response containing a unique UUID and a dummy verification code sent to the provided email.

@RestController
@RequestMapping(Api.BASE)
public class VerificationController {

  @PostMapping(Api.CREATE_VERIFICATION)
  public ResponseEntity<VerificationResponse> createVerification(
      @RequestBody VerificationRequest verification) {

    var response = VerificationResponse
        .builder()
        .mail(verification.getMail())
        .uuid(UUID.randomUUID().toString())
        .build();

    // Simulate sending a verification code to the user's email
    return new ResponseEntity<>(response, HttpStatus.OK);
  }
}

3. Registration Models

The RegistrationAddress class holds the user's address details, while the RegistrationRequest contains both the address and the verification response (which includes the verification code, UUID, and email):

public class RegistrationAddress {

  private String street;
  private String houseNumber;
  private String zip;
  private String city;
  private String country;
}

public class VerificationResponse {

  private String code;
  private String uuid;
  private String mail;
}

public class RegistrationRequest {

  private RegistrationAddress address;
  private VerificationResponse verification;
}

4. Registration Controller

The RegistrationController handles the registration process. Once the user sends the verification code, the backend will validate it and, if valid, complete the registration process.

@RestController
@RequestMapping(Api.BASE)
public class RegistrationController {

  @PostMapping(Api.CREATE_REGISTRATION)
  public ResponseEntity<Void> createRegistration(@RequestBody RegistrationRequest registration) {
    // Simulate the process of verifying the code entered by the user
    return new ResponseEntity<>(HttpStatus.CREATED);
  }
}

Test Implementation

The table below shows the same test scenario written in two different ways. On the left side, the test is written using MockMvc, which is the traditional way to perform HTTP tests in Spring. On the right side, the same test is written using the AAA-Mock framework, which is an open-source testing framework designed to make the test code more readable and concise.

It is important to note that in both examples, the use of helper private methods has been intentionally avoided to keep the tests straightforward and focused on the core testing logic. This ensures clarity and helps demonstrate the difference between the two testing approaches.

Test with MockMvc

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@Test
@SneakyThrows
void GIVEN_registration_with_valid_verification_WHEN_createRegistration_THEN_return_status_201() {

  var verificationRequest = VerificationRequest.builder()
      .firstname(FIRSTNAME)
      .lastname(LASTNAME)
      .mail(EMAIL)
      .build();

  var verificationResponseJson = mockMvc.perform(
          MockMvcRequestBuilders.post(POST_CREATE_VERIFICATION)
              .contentType(MediaType.APPLICATION_JSON)
              .content(objectMapper.writeValueAsString(verificationRequest)))
      .andReturn()
      .getResponse()
      .getContentAsString();

  var verificationResponse = objectMapper.readValue(verificationResponseJson,
      VerificationResponse.class);

  var registrationRequest = RegistrationRequest.builder()
      .verification(VerificationResponse.builder()
          .code(VALID_CODE)
          .mail(verificationResponse.getMail())
          .uuid(verificationResponse.getUuid())
          .build())
      .build();

  mockMvc.perform(MockMvcRequestBuilders.post(POST_CREATE_REGISTRATION)
          .contentType(MediaType.APPLICATION_JSON)
          .content(objectMapper.writeValueAsString(registrationRequest)))
      .andExpect(status().isCreated());
}

Test with AAA-MockMvc

@Test
@SneakyThrows
void GIVEN_registration_with_valid_verification_WHEN_createRegistration_THEN_return_status_201() {

  var verificationRequest = VerificationRequest.builder()
      .firstname(FIRSTNAME)
      .lastname(LASTNAME)
      .mail(EMAIL)
      .build();

  var verificationResponse = post()
      .arrange()
      .arrangeUrl(POST_CREATE_VERIFICATION)
      .arrangeBody()
      .arrangeJson(verificationRequest)
      .act()
      .actPerform()
      .answer()
      .answerAsObject(VerificationResponse.class);

  var registrationRequest = RegistrationRequest
      .builder()
      .verification(VerificationResponse.builder()
          .code(VALID_CODE)
          .mail(verificationResponse.getMail())
          .uuid(verificationResponse.getUuid())
          .build())
      .build();

  post()
      .arrange()
      .arrangeUrl(POST_CREATE_REGISTRATION)
      .arrangeBody()
      .arrangeJson(registrationRequest)
      .act()
      .actPerform()
      .asserts()
      .assertStatus()
      .assertStatusIsCreated();
}

License

This project is licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.

About

This project provides a framework for structuring unit tests following the AAA (Arrange, Act, Assert) pattern with a focus on clarity and readability.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages