Úvod: Proč psát testy (a proč je psát dobře)?

V dnešním rychlém světě vývoje softwaru je kvalita klíčová. Jedním z nejlepších způsobů, jak ji zajistit, je Test-Driven Development (TDD) a psaní čistých, udržitelných testů.

Výchozí bod: Implementace nové funkcionality (a první testy)

Představme si, že naším úkolem je implementovat API endpoint pro vytváření nového „Eligible Collateral“ pro danou protistranu.

Produkční kód (EligibleCollateralSpringFacade.java)

public class EligibleCollateralSpringFacade {
    public EligibleCollateralDetail create(final Counterparty counterparty, final EligibleCollateralCreate create) {
        validateCounterpartyId(counterparty);
        final var eligibleCollateral = create.toDomain(counterparty);
        final var stored = this.service.save(eligibleCollateral);
        return EligibleCollateralDetail.from(stored);
    }

    private void validateCounterpartyId(final Counterparty id) {
        // Validace existence protistrany
    }
}

První testy (EligibleCollateralsAcceptanceTest.java)

public class EligibleCollateralsAcceptanceTest {
    @Autowired MockMvc mockMvc;
    @Autowired AuthenticatedTestApi authenticationApi;

    @Test
    public void created_eligible_collaterals_contains_filled_in_information() throws Exception {
        authenticationApi.logInAsRisk();
        Long counterpartyId = 4L;
        String requestJson = "{"name": "Eligible Name", "isin": "isin123"}";

        mockMvc.perform(post("/api/non-clients/{id}/eligible-collaterals", counterpartyId)
                .content(requestJson)
                .contentType(MediaType.APPLICATION_JSON)
                .session(authenticationApi.getSession()))
            .andExpect(status().isOk())
            .andExpect(jsonPath("name").value("Eligible Name"))
            .andExpect(jsonPath("isin").value("isin123"));
    }
}

Krok 1: Refaktoring – Vytvoření Test API

EligibleCollateralsTestApi.java

@Component
public class EligibleCollateralsTestApi {
    @Autowired MockMvc mockMvc;
    @Autowired AuthenticatedTestApi authenticationApi;

    public ResultActions create(Long counterpartyId, String eligibleCollateralJson) {
        return mockMvc.perform(post("/api/non-clients/{id}/eligible-collaterals", counterpartyId)
            .content(eligibleCollateralJson)
            .contentType(MediaType.APPLICATION_JSON)
            .session(authenticationApi.getSession()));
    }

    public AuthenticatedTestApi authentication() { return authenticationApi; }
    public Long existingNonClient() { return 4L; }
}

Upravený test

@Test
public void created_eligible_collaterals_contains_filled_in_information() throws Exception {
    api.authentication().logInAsRisk();
    Long counterpartyId = api.existingNonClient();
    String requestJson = "{"name": "Eligible Name", "isin": "isin123"}";

    api.create(counterpartyId, requestJson)
        .andExpect(status().isOk())
        .andExpect(jsonPath("name").value("Eligible Name"))
        .andExpect(jsonPath("isin").value("isin123"));
}

Krok 2: Refaktoring – Zavedení Response Objectu

EligibleCollateralsDetailReponse.java

public class EligibleCollateralsDetailReponse {
    private final ResultActions resultActions;

    public EligibleCollateralsDetailReponse(ResultActions resultActions) {
        this.resultActions = resultActions;
    }

    public EligibleCollateralsDetailReponse expectStatusOk() {
        expect(status().isOk());
        return this;
    }

    public EligibleCollateralsDetailReponse expectName(String expected) {
        expect(jsonPath("name").value(expected));
        return this;
    }

    public void expectIsin(String expected) {
        expect(jsonPath("isin").value(expected));
    }

    private void expect(ResultMatcher matcher) {
        this.resultActions.andExpect(matcher);
    }
}

Upravený test

@Test
public void created_eligible_collaterals_contains_filled_in_information() {
    api.authentication().logInAsRisk();
    Long counterpartyId = api.existingNonClient();
    String requestJson = "{"name": "Eligible Name", "isin": "isin123"}";

    api.create(counterpartyId, requestJson)
        .expectStatusOk()
        .expectName("Eligible Name")
        .expectIsin("isin123");
}

Krok 3: Refaktoring – Test Data Builder (EligibleDraft.java)

EligibleDraft.java

@Data
@AllArgsConstructor
@Accessors(fluent = true)
public class EligibleDraft {
    @Nullable @JsonProperty("name")
    private String name;

    @Nullable @JsonProperty("isin")
    private String isin;

    public String toJson() {
        final var om = new ObjectMapper();
        return om.writeValueAsString(this);
    }
}

Rozšíření Test API

public EligibleDraft prefilledEligibleDraft() {
    return new EligibleDraft("Default Eligible Name", "DefaultISIN123");
}

public EligibleCollateralsDetailReponse create(Long counterparty, EligibleDraft draft) {
    return create(counterparty, draft.toJson());
}

Test s builderem

@Test
public void created_eligible_collaterals_contains_filled_in_information() {
    api.authentication().logInAsRisk();
    final var counterparty = api.existingNonClient();
    final var draft = api.prefilledEligibleDraft()
                         .name("Eligible Name")
                         .isin("isin123");

    api.create(counterparty, draft)
        .expectStatusOk()
        .expectName("Eligible Name")
        .expectIsin("isin123");
}

TDD v praxi: Přidání kontroly oprávnění

Test na nedostatečná oprávnění

@Test
public void create_eligible_with_insufficient_permissions_collaterals_returns_error() {
    api.authentication().logInAsBranchWorker();
    final var counterparty = api.existingNonClient();
    final var draft = api.prefilledEligibleDraft();

    api.create(counterparty, draft)
        .expectStatusForbidden();
}

Implementace zabezpečení

@Secured(DafosPermissions.EDIT_COUNTERPARTY_LIMITS)
public EligibleCollateralDetail create(final Counterparty counterparty, final EligibleCollateralCreate create) {
    validateCounterpartyId(counterparty);
    final var eligibleCollateral = create.toDomain(counterparty);
    final var stored = this.service.save(eligibleCollateral);
    return EligibleCollateralDetail.from(stored);
}

Závěr: Investice, která se vyplatí

Dobře navržená testovací infrastruktura přináší:

  • Větší důvěru při refaktoringu.
  • Rychlejší odhalování chyb.
  • Lepší dokumentaci systému.
  • Snazší onboarding nových vývojářů.
  • Nižší náklady na údržbu a rozvoj aplikace.