WEEK 10Monday

Testing What the Agent Wrote

Week 10 · From a sorting method to Project Pulse

WEEK 10Monday

How do you know a method is correct?

Code review: static

Read the code. Reason about what it would do.

Testing: dynamic

Run the code. Watch what it does.

WEEK 10Monday

Selection sort

public static int[] sort(int[] input) {
    int[] a = input.clone();
    for (int i = 0; i < a.length - 1; i++) {
        int min = i;
        for (int j = i + 1; j < a.length; j++) {
            if (a[j] < a[min]) {
                min = j;
            }
        }
        int tmp = a[i];
        a[i] = a[min];
        a[min] = tmp;
    }
    return a;
}
WEEK 10Monday

Our first unit tests

class SelectionSortTest {

    @Test
    void sortsAnUnsortedArray() {
        int[] result = SelectionSort.sort(new int[] {5, 2, 9, 1});
        assertThat(result).containsExactly(1, 2, 5, 9);
    }

    @Test
    void keepsDuplicates() {
        int[] result = SelectionSort.sort(new int[] {3, 1, 3, 1});
        assertThat(result).containsExactly(1, 1, 3, 3);
    }

    @Test
    void sortsAnEmptyArray() {
        assertThat(SelectionSort.sort(new int[] {})).isEmpty();
    }
}
WEEK 10Monday

What is a unit test?

  • Unit: the smallest piece you can test alone, usually one method or one class.
  • Test case: an input, plus the output you expect.
  • Oracle: whatever decides that the output is right.
Key point

A test is inputs plus an oracle.

WEEK 10Monday

A weak oracle

@Test
void resultIsSorted() {
    int[] result = SelectionSort.sort(new int[] {5, 2, 9, 1});
    assertThat(result).isSorted();
}
Ask the room

Which broken sort passes this test?

  • return new int[0]; is sorted. Green.
  • The real oracle: the output is sorted and holds exactly the input's elements.
WEEK 10Monday

Why unit tests matter

  • Fast: hundreds run in seconds, so you run them on every change.
  • Precise: a red test names the method that broke.
  • Repeatable: same input, same answer, on any machine, forever.
  • A specification you can run: the test says what the code must do.
WEEK 10Monday

The JUnit 5 and AssertJ you need

assertThat(result).containsExactly(1, 2, 5, 9);
assertThat(result).hasSize(4);
assertThat(result).isSorted();
assertThat(result).contains(5).doesNotContain(7);
assertThat(evaluation.getWeek()).isEqualTo("2023-W31");
assertThat(catchThrowable(() -> SelectionSort.sort(null)))
        .isInstanceOf(NullPointerException.class);
  • @Test marks a test; @BeforeEach gives every test fresh setup.
  • @ParameterizedTest runs one test body over many rows of input.
  • JUnit 5 runs the tests; AssertJ's assertThat checks the results, as in Project Pulse.
WEEK 10Monday

Project Pulse: a method with a database behind it

public PeerEvaluation addPeerEvaluation(PeerEvaluation newPeerEvaluation) {
    // Make sure the submission week is one of the course section's active weeks and is still open
    requireOpenSubmissionWindow(...);

    // ... the evaluator is on a team, and the evaluator and evaluatee are on the same team ...

    // Make sure this is not a duplicate evaluation
    if (this.evaluationRepository.findByWeekAndEvaluatorIdAndEvaluateeId(
            week, evaluatorId, evaluateeId).isPresent()) {
        throw new PeerEvaluationIllegalArgumentException(
            "You have already submitted an evaluation for ... in this week.");
    }
    return this.evaluationRepository.save(newPeerEvaluation);
}

Shortened from EvaluationService on Project Pulse main.

WEEK 10Monday

Mock the repository

@ExtendWith(MockitoExtension.class)
class EvaluationServiceTest {

    @Mock
    PeerEvaluationRepository evaluationRepository;
    @Mock
    StudentRepository studentRepository;
    @Mock
    Clock clock; // Mock the clock bean

    @InjectMocks
    EvaluationService evaluationService;
  • @Mock: a fake whose answers the test controls.
  • @InjectMocks: the real class under test, handed the fakes.
  • given(mock.call()).willReturn(value): "when asked this, answer that".
WEEK 10Monday

Given, when, then

@Test
void testAddPeerEvaluationDuplicatedEvaluation() {
    // Given
    PeerEvaluation newPeerEvaluation = new PeerEvaluation("2023-W31", john, eric, ...);
    given(this.evaluationRepository.findByWeekAndEvaluatorIdAndEvaluateeId("2023-W31", 3, 4))
            .willReturn(Optional.of(newPeerEvaluation));

    // When
    Throwable throwable = catchThrowable(
            () -> this.evaluationService.addPeerEvaluation(newPeerEvaluation));

    // Then
    assertThat(throwable)
            .isInstanceOf(PeerEvaluationIllegalArgumentException.class)
            .hasMessage("You have already submitted an evaluation for "
                    + newPeerEvaluation.getEvaluatee().getFirstName() + " in this week.");
}

Shortened from EvaluationServiceTest (the fixed clock setup is left out).

WEEK 10Monday

Mock less than you can

  • Mock the slow and the uncontrollable: database, network, email, the clock.
  • Use real objects when they are fast and predictable.
  • Assert on results first; check calls only when the call is the outcome.
Pitfall

The clock is a dependency too. A test that reads today's date passes this week and fails some other week.

WEEK 10Monday

How to pick test cases: partitions

letterGrade(score): 90–100 A · 80–89 B · 70–79 C · 60–69 D · 0–59 F · anything else invalid

Key point

A partition is a group of inputs the code should treat the same way. One test per group.

  • Seven partitions: A, B, C, D, F, below 0, above 100.
  • One input from each: 95 · 85 · 75 · 65 · 30 · -5 · 120
WEEK 10Monday

Boundaries, edges, and corners

Boundaries: both sides of every edge

-1, 0 · 59, 60 · 69, 70 · 79, 80 · 89, 90 · 100, 101

Corners: the odd shapes

Empty · one element · all equal · null · huge

A QA engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. The first real customer asks where the bathroom is. The bar bursts into flames.

WEEK 10Monday

One test, every boundary

@ParameterizedTest
@CsvSource({ "0, F", "59, F", "60, D", "69, D", "70, C", "79, C",
             "80, B", "89, B", "90, A", "100, A" })
void letterGradeFollowsTheScale(int score, String grade) {
    assertThat(Grades.letterGrade(score)).isEqualTo(grade);
}

@ParameterizedTest
@ValueSource(ints = { -1, 101 })
void letterGradeRejectsScoresOutsideZeroToHundred(int score) {
    assertThat(catchThrowable(() -> Grades.letterGrade(score)))
            .isInstanceOf(IllegalArgumentException.class);
}
WEEK 10Monday

Integration tests: the real thing, wired together

  • Real Spring, real MySQL in a Docker container, real HTTP requests.
  • Each test logs in as a seeded user and calls an API route.
  • Each test rolls back, so the next one starts from the same data.
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("dev")
@Tag("integration")
@Transactional
public abstract class AbstractIntegrationTest {
    @ServiceConnection
    static final MySQLContainer<?> mysql = SharedContainers.MYSQL;

Shortened from Project Pulse's AbstractIntegrationTest.

WEEK 10Monday

Where do the test cases come from?

  • The use case: every main step and every extension is at least one test.
  • The design: it lists the tests, each with its layer, before any code.
  • The reminder's design had 20 test rows. Review added 4, all about time.
Key point

Trace every test back to a use case. A flow with no test was never checked.

WEEK 10Monday

Access control: John asks for Eric's peer evaluations

@Test
void testStudentJohnGenerateOwnWeeklyPeerEvaluationSummary() throws Exception {
    this.mockMvc.perform(get(this.baseUrl + "/evaluations/students/4/week/2023-W31")
                    .header(HttpHeaders.AUTHORIZATION, this.studentJohnToken))
            .andExpect(jsonPath("$.code").value(StatusCode.SUCCESS))
            .andExpect(jsonPath("$.data.privateComments").isEmpty()); // privateComments MUST be empty
}

@Test
void testStudentJohnGenerateAnotherStudentsWeeklyPeerEvaluationSummary() throws Exception {
    this.mockMvc.perform(get(this.baseUrl + "/evaluations/students/5/week/2023-W31")
                    .header(HttpHeaders.AUTHORIZATION, this.studentJohnToken))
            .andExpect(jsonPath("$.code").value(StatusCode.FORBIDDEN))
            .andExpect(jsonPath("$.message").value("No permission."));
}

Shortened from EvaluationIntegrationTest.

WEEK 10Monday

What else may John not do?

Ask the room

The use case says students see only their own evaluations. List every request that should be forbidden.

  • Update another evaluator's evaluation: testEvaluatorJohnUpdatesAnotherEvaluatorsEvaluation
  • Read another student's detailed evaluations: testStudentJohnGetDetailedWeeklyPeerEvaluationsOfSingleStudent
  • Generate the whole section's evaluations: testStudentJohnGenerateWeeklyPeerEvaluationForSection
  • Smuggle another evaluator's rating ids into his own: testStudentJohnCannotAdoptAnotherEvaluatorsRatingsThroughCreate
WEEK 10Monday

Test coverage

Key point

Coverage shows what ran, not what was checked.

What it is

The share of lines (or branches) your tests executed.

What it is for

Finding code no test touches. Never a score to chase.

WEEK 10Monday

100% coverage, and the rule is checked nowhere

// Given: 2026-W38 is the last active week; Sep 23 is in the week after
Section section = section(1, MONDAY, WEDNESDAY);
section.setActiveWeeks(List.of("2026-W37", PREVIOUS_WEEK));
given(sectionService.findReminderEligibleSectionsForWeek(PREVIOUS_WEEK))
        .willReturn(List.of(section));
// When
scheduler(LocalDate.of(2026, 9, 23), true).sendWeeklyReminders();
// Then
verify(emailService, times(1)).sendReminderEmail(eq("s1@abc.edu"), anyString(), anyString());
  • The test claims to check "only active weeks get reminders".
  • The rule lives in one query, and the test mocks that query.
  • Delete the query's condition: this test, and the whole suite, stay green.

I mocked the database, the mail server, and the clock. The test passed. So did the bug.

WEEK 10Monday

End to end: Cypress drives the browser

it('sends no reminder when the instructor cancels (5a)', () => {
  openAndChoosePeerEvaluation(owingStatus)

  cy.get('[data-cy=remind-cancel]').click()

  cy.get('[data-cy=remind-dialog]').should('not.be.visible')
  cy.get('@reminders.all').should('have.length', 0)
})

From frontend/cypress/e2e/reminders.spec.cy.js.

  • A real browser clicks, types, and reads the page, the way a user does.
  • The slowest and most fragile tests, so keep them for what only the browser shows.
  • Project Pulse has 19 Cypress tests, and its build does not run them yet.
WEEK 10Monday

Three layers, on Project Pulse

Layer Checks Project Pulse
Unit One class, dependencies mocked; milliseconds about 125
Integration Real Spring, real MySQL, HTTP; seconds about 320
End to end The browser and the whole stack; slowest 19 Cypress, not in CI
Key point

Find where the rule lives. Test it at that layer.

WEEK 10Monday

Regression tests: a fixed bug stays fixed

  • The bug: the report built "Last, First" by splitting the name on a space.
  • "Mary Ann Smith" became "Ann, Mary". "Cher" became "undefined, Cher".
  • The fix: the server now sends first and last names separately. No test pins the bug.
Key point

Every bug fix comes with a test that fails before the fix and passes after.

WEEK 10Monday

The suite runs on every pull request

on:
  pull_request:
    branches:
      - main
...
      - name: Build and test with Maven
        working-directory: ./backend
        run: mvn --batch-mode package

From Project Pulse's .github/workflows/maven-build.yml.

WEEK 10Monday

What stays human

Delegate

The candidate list, boilerplate, parameterized cases, running the suite.

Keep

The oracle, which tests are in scope, and every change to a failing test.

WEEK 10Monday

For your team

  • Every extension of your proving slice's use case has a test named after it, in given/when/then.
  • Your charter: both BDDMockito halves, and no failing test edited without a reason in the pull request.
  • Assignment 3: name each kept test's oracle, and break the code once.
Key point

When an agent writes the code, the tests are the part you read.

Testing What the Agent Wrote

→ space PgDn
Next step or slide
← PgUp
Back
↓ S
Skip to the next slide
O
Overview of all slides
N
Speaker notes
B
Black the screen
Ctrl+P
Print / PDF handout, one slide per page