Skip to main content

Overview

The Blackjack API project follows a comprehensive testing strategy that mirrors the hexagonal architecture. Tests are organized into three main layers: Domain, Application, and Infrastructure. This approach ensures that business logic is thoroughly tested independently of external dependencies.

Testing Stack

  • JUnit 5 - Testing framework
  • Mockito - Mocking framework for unit tests
  • Reactor Test - Testing reactive streams with StepVerifier
  • Spring WebFlux Test - WebTestClient for controller integration tests

Running Tests

Run all tests using Maven:
./mvnw test
Run tests with detailed output:
./mvnw test -X
Run a specific test class:
./mvnw test -Dtest=GameControllerTest

Test Organization

Tests are structured to match the application architecture:
src/test/java/
└── cat/itacademy/s05/t01/blackjack_api/
    ├── application/usecase/      # Application layer tests
    ├── domain/model/              # Domain layer tests
    └── infrastructure/web/        # Infrastructure layer tests

Domain Layer Tests

Domain tests are pure unit tests with no framework dependencies. They verify core business logic including game rules, card calculations, and domain model behavior.

Example: Game Logic Test

@Test
void player_bust_finishes_game_as_dealer_wins() {
    Deck d = deck(
            new Card(Rank.TEN, Suit.CLUBS),
            new Card(Rank.TWO, Suit.HEARTS),
            new Card(Rank.NINE, Suit.SPADES),
            new Card(Rank.THREE, Suit.DIAMONDS),
            new Card(Rank.FIVE, Suit.CLUBS)
    );

    Game g = Game.newGame(PlayerId.newId(), d);

    assertEquals(GameStatus.IN_PROGRESS, g.status());

    g = g.hit();

    assertEquals(GameStatus.DEALER_WINS, g.status());
}

Example: Hand Calculation Test

@Test
void ace_can_be_1_or_11(){
    Hand h = Hand.empty()
            .add(new Card(Rank.ACE, Suit.SPADES))
            .add(new Card(Rank.KING, Suit.CLUBS));

    assertEquals(21, h.score());
    assertTrue(h.isBlackjack());

    Hand h2 = Hand.empty()
            .add(new Card(Rank.ACE, Suit.SPADES))
            .add(new Card(Rank.ACE, Suit.HEARTS))
            .add(new Card(Rank.NINE, Suit.DIAMONDS));

    assertEquals(21, h2.score());
    assertFalse(h2.isBust());
}

Domain Test Coverage

Domain tests cover:
  • ✅ Game creation and initialization
  • ✅ Natural blackjack scenarios
  • ✅ Player and dealer bust conditions
  • ✅ Dealer hit/stand logic (dealer hits until 17+)
  • ✅ Hand score calculations with Ace handling
  • ✅ Invalid move prevention
  • ✅ Value object validation

Application Layer Tests

Application layer tests verify use case workflows using Mockito to mock repository ports. These tests use Reactor Test’s StepVerifier for reactive stream validation.

Example: Use Case Test

@Test
void should_create_game_for_player() {
    Player player = new Player(PlayerId.newId(), new PlayerName("Alan"), 0, 0);

    when(playerRepo.findOrCreateByName(any(PlayerName.class)))
            .thenReturn(Mono.just(player));
    when(gameRepo.save(any(Game.class)))
            .thenAnswer(invocationOnMock -> Mono.just(invocationOnMock.getArgument(0)));

    StepVerifier.create(useCase.execute(new CreateGameCommand("Alan")))
            .assertNext(res -> {
                assertFalse(res.gameId().isBlank());
                assertEquals(player.id().value(), res.playerId());
                assertFalse(res.status().isBlank());
            })
            .verifyComplete();

    verify(playerRepo).findOrCreateByName(new PlayerName("Alan"));
    verify(gameRepo).save(any(Game.class));
}

Application Test Coverage

Use case tests include:
  • CreateNewGameUseCase - Game creation and player initialization
  • GetGameStateUseCase - Game state retrieval
  • PlayMoveUseCase - HIT and STAND move execution
  • DeleteGameUseCase - Game deletion
  • ChangePlayerNameUseCase - Player name updates
  • ViewRankingUseCase - Player ranking calculations

Infrastructure Layer Tests

Infrastructure tests verify REST endpoints and HTTP integration using WebTestClient. These tests mock use cases and validate request/response handling.

Example: Controller Test

@WebFluxTest(controllers = GameController.class)
@Import(GlobalExceptionHandler.class)
public class GameControllerTest {

    @Autowired
    WebTestClient webTestClient;

    @MockitoBean
    CreateNewGameUseCase createNewGameUseCase;

    @Test
    void should_create_game_and_return_201() {
        when(createNewGameUseCase.execute(any(CreateGameCommand.class)))
                .thenReturn(Mono.just(new CreateGameResult("g1","p1","IN_PROGRESS")));

        webTestClient.post()
                .uri("/game/new")
                .bodyValue(new CreateGameRequest("Ccr"))
                .exchange()
                .expectStatus().isCreated()
                .expectBody()
                .jsonPath("$.gameId").isEqualTo("g1")
                .jsonPath("$.playerId").isEqualTo("p1")
                .jsonPath("$.status").isEqualTo("IN_PROGRESS");
    }
}

Example: Error Handling Test

@Test
void should_return_404_when_game_not_found() {
    when(getGameStateUseCase.execute("missing"))
            .thenReturn(Mono.error(new GameNotFoundException("missing")));

    webTestClient.get()
            .uri("/game/missing")
            .exchange()
            .expectStatus().isNotFound()
            .expectBody()
            .jsonPath("$.status").isEqualTo(404)
            .jsonPath("$.code").isEqualTo("GAME_NOT_FOUND")
            .jsonPath("$.path").isEqualTo("/game/missing");
}

Infrastructure Test Coverage

Controller tests validate:
  • ✅ HTTP status codes (200, 201, 204, 400, 404, 409)
  • ✅ Request/response JSON mapping
  • ✅ Validation error handling
  • ✅ Domain exception mapping via GlobalExceptionHandler
  • ✅ Dealer card hiding logic (second card hidden during gameplay)
  • ✅ Complete game state serialization

Test Coverage by Controller

GameController Tests

  • POST /game/new - Game creation
  • GET /game/{id} - Game state retrieval
  • POST /game/{id}/play - HIT and STAND moves
  • DELETE /game/{id}/delete - Game deletion

PlayerController Tests

  • PUT /player/{playerId} - Player name changes

RankingController Tests

  • GET /ranking - Player ranking retrieval

Testing Best Practices

  • Pure unit tests with no Spring framework dependencies
  • Test business rules exhaustively
  • Use domain-specific test data builders
  • Verify invariants and preconditions
  • Mock repository ports using Mockito
  • Use StepVerifier for reactive stream testing
  • Verify repository interactions with verify()
  • Test both success and failure scenarios
  • Use @WebFluxTest for controller slicing
  • Import GlobalExceptionHandler for error handling tests
  • Mock use cases, not repositories
  • Validate HTTP semantics (status codes, headers)
  • Test JSON serialization/deserialization

Reactive Testing with StepVerifier

The project uses Reactor Test to verify reactive streams:
StepVerifier.create(useCase.execute(command))
    .assertNext(result -> {
        // Assertions on emitted value
    })
    .verifyComplete();
For error scenarios:
StepVerifier.create(useCase.execute(invalidCommand))
    .expectError(InvalidPlayerNameException.class)
    .verify();

Continuous Testing

Tests are automatically run on:
  • Local development via ./mvnw test
  • CI/CD pipelines (if configured)
  • Pre-commit hooks (if configured)

Test Data Management

Tests use in-memory test data rather than external databases:
  • Domain tests use pure Java objects
  • Application tests use Mockito mocks
  • Infrastructure tests use mocked use cases
This ensures:
  • ⚡ Fast test execution
  • 🔒 Test isolation
  • 🔄 No side effects between tests

Next Steps

Contributing Guide

Learn about development workflow and coding standards

Architecture

Understand the hexagonal architecture

Build docs developers (and LLMs) love