QA Interview Hub · Knowledge base · Quizzes

Java basics

Assertions and API tests

AssertJ: readable assertions and soft assertions

AssertJ is a library of fluent assertions. Everything starts with assertThat(actual), followed by a chain of methods that the IDE suggests:

import static org.assertj.core.api.Assertions.assertThat;

assertThat(user.name()).isEqualTo("Anna");
assertThat(names).hasSize(3).contains("Anna").doesNotHaveDuplicates();
assertThat(price).isBetween(10, 100);
assertThat(order).usingRecursiveComparison().isEqualTo(expectedOrder);

Its advantage over a plain assertEquals is a clear failure message ("Expecting … to contain …") and the "actual first, then expected" order without mixing up arguments. usingRecursiveComparison() compares objects field by field without an overridden equals, and extracting pulls fields out of a list of objects. For exceptions: assertThatThrownBy(() -> ...).isInstanceOf(X.class).hasMessageContaining("...").

Soft assertions do not stop at the first failure; they collect all of them and report together. Useful when several independent fields are checked and you want the whole picture in one run:

SoftAssertions.assertSoftly(softly -> {
    softly.assertThat(user.name()).isEqualTo("Anna");
    softly.assertThat(user.role()).isEqualTo("admin");
    softly.assertThat(user.active()).isTrue();
});

In assertSoftly the verification runs automatically at the end of the block. If you create new SoftAssertions() by hand you must call softly.assertAll(): without it failures are ignored and the test turns green. In JUnit 5 you can attach SoftAssertionsExtension with an @InjectSoftAssertions field.

When not to use them: when the next check depends on the previous one (there is no point checking a field if the object is null): there you need a regular, hard assertion.

JUnit 5 and TestNG have their own Assertions, and Hamcrest has assertThat(actual, is(expected)) (REST Assured uses its matchers). For new code AssertJ is more convenient.

REST Assured, the Builder for data and Lombok

REST Assured is a BDD-style DSL for API tests. The given / when / then chain:

  • given() — preconditions: base URL, headers, parameters, body, authorisation;
  • when() — the action: get("/users/1"), post("/users");
  • then() — checks: statusCode(201), header(...), body("id", notNullValue()) (Hamcrest matchers, JsonPath/GPath-style paths).
User created = given()
    .spec(requestSpec)
    .body(newUser)                      // a POJO is serialised to JSON
.when()
    .post("/users")
.then()
    .statusCode(201)
    .extract().as(User.class);          // deserialised into a POJO

assertThat(created.name()).isEqualTo(newUser.name());

Serialisation needs a library on the classpath (Jackson, Gson or JSON-B). Shared settings go into a RequestSpecification (via RequestSpecBuilder): base URI, content type, authorisation, log().ifValidationFails() to see the request and response only on failure. A separate extract().path("id") takes one value. Schema checks use the json-schema-validator module.

A Builder for test data. A constructor with ten parameters is unreadable. A builder sets valid defaults and the test changes only the field that matters for the scenario:

User admin = UserBuilder.aUser().withRole(Role.ADMIN).build();
User noEmail = UserBuilder.aUser().withEmail(null).build();  // a negative case

Generate unique values (an email with a suffix, a random login) in the builder so parallel tests do not collide.

Lombok generates code at compile time through annotation processing: @Getter/@Setter, @Builder, @Value (an immutable class), @Slf4j (a logger field). It is added as a provided dependency and needs an IDE plugin; in Maven its processor is best declared explicitly in the compiler's annotationProcessorPaths (since JDK 23 processors from the classpath are not run by default). Be careful with @Data on mutable classes: it generates equals/hashCode over all fields. For simple data a modern alternative is a record.