Generate Edge-Case Tests
isMinimumAge checks whether a candidate is old enough for a test - age 17 or more, the comment above it says. Three hand-written tests already pass; none of them tries the boundary. Read domain.ek9. To play it in the IDE: Help > Lessons > Quality and reports…, or ek9 -ide -lesson edge-case-tests.
Start from
The code the lesson starts from: copy it into a file of your own and follow the steps.
domain.ek9
#!ek9 <?- Driving Test Centre - a candidate's age. REQUIREMENT: a candidate is old enough to book a driving test from age 17 (inclusive) up to 70 (inclusive) - the Centre does not test anyone outside that range at all. -?> defines module quality.centre.domain defines constant MINIMUM_AGE <- 17 defines type CandidateAge as Integer constrain as >= 17 and <= 70 defines function // REQUIREMENT: eligible for a test from MINIMUM_AGE (inclusive) - the Centre's own minimum. isMinimumAge() -> age as CandidateAge <- rtn as Boolean: age > MINIMUM_AGE //EOF
vehicles.ek9
#!ek9 <?- Driving Test Centre - vehicle categories, in their own file (same module as domain.ek9, which a module may span) so the Generate Edge-Case Tests… lesson can scope its whole-file run to domain.ek9 alone - just CandidateAge and isMinimumAge - without these four trivial classes. -?> defines module quality.centre.domain defines trait Vehicle category() as pure <- rtn as String? defines class Car with trait of Vehicle override category() as pure <- rtn as String: "car" default operator ? Motorcycle with trait of Vehicle override category() as pure <- rtn as String: "motorcycle" default operator ? Lorry with trait of Vehicle override category() as pure <- rtn as String: "lorry" default operator ? QuadBike with trait of Vehicle override category() as pure <- rtn as String: "quad-bike" default operator ? //EOF
scoring.ek9
#!ek9 <?- Driving Test Centre - theory test scoring, in its own file (same module as domain.ek9) so the Generate Edge-Case Tests… lesson can scope its whole-file run to domain.ek9 alone, without this function's require/division in the same scope. -?> defines module quality.centre.domain defines function // REQUIREMENT: a candidate's average theory test score across every attempt made so far - // attempts is always at least one (a candidate who has never sat the test has no average yet). averageScore() -> totalMarks as Float attempts as Float <- rtn as Float? require attempts > 0.0 rtn: totalMarks / attempts //EOF
service.ek9
#!ek9 <?- Driving Test Centre - the booking service: a fee schedule, injected into an application, used by a program. Two modules (quality.centre.service depends on quality.centre.domain) for the quality report's Module Dependencies page; AbstractFeeSchedule/StandardFeeSchedule for its Type Hierarchy and DI Wiring pages. -?> defines module quality.centre.service references quality.centre.domain::CandidateAge defines constant JUNIOR_AGE <- 25 SENIOR_AGE <- 60 defines class BookingService schedule as AbstractFeeSchedule! quote() -> age as CandidateAge category as String <- rtn as Money: schedule.feeFor(age, category) default operator ? defines component AbstractFeeSchedule as abstract feeFor() as abstract -> age as CandidateAge category as String <- rtn as Money? default operator ? // REQUIREMENT: the test fee depends on the candidate's age band and the vehicle category - // three age bands across four categories, the Centre's full published fee schedule. StandardFeeSchedule is AbstractFeeSchedule override feeFor() -> age as CandidateAge category as String <- rtn as Money: 0.00#GBP isCar <- category == "car" isMotorcycle <- category == "motorcycle" isLorry <- category == "lorry" isQuadBike <- category == "quad-bike" isJunior <- age <= JUNIOR_AGE isSenior <- age <= SENIOR_AGE if isCar and isJunior rtn: 34.00#GBP else if isCar and isSenior rtn: 40.00#GBP else if isCar rtn: 38.00#GBP else if isMotorcycle and isJunior rtn: 28.00#GBP else if isMotorcycle and isSenior rtn: 32.00#GBP else if isMotorcycle rtn: 29.00#GBP else if isLorry and isJunior rtn: 60.00#GBP else if isLorry and isSenior rtn: 65.00#GBP else if isLorry rtn: 62.00#GBP else if isQuadBike and isJunior rtn: 24.00#GBP else if isQuadBike and isSenior rtn: 27.00#GBP else if isQuadBike rtn: 26.00#GBP default operator ? defines application CentreApp register StandardFeeSchedule() as AbstractFeeSchedule defines program QuoteFee() with application of CentreApp stdout <- Stdout() service <- BookingService() candidate <- CandidateAge(30) fee <- service.quote(candidate, "car") stdout.println(`Fee: ${fee}`) //EOF
dev/tests.ek9
#!ek9 <?- Driving Test Centre - a FEW hand-written tests, deliberately leaving real gaps: isMinimumAge is only tried at one interior age (never the boundary, where the off-by-one lives), averageScore is only tried with ordinary marks (never the require/division edge), and only Car's category() is tried at all - Motorcycle, Lorry, QuadBike and the whole fee schedule are untested. Coverage, input variety and the budget bars all show what is missing. -?> defines module quality.centre.tests references quality.centre.domain::averageScore quality.centre.domain::CandidateAge quality.centre.domain::Car quality.centre.domain::isMinimumAge defines program @Test AdultIsEligible() candidate <- CandidateAge(30) assert isMinimumAge(candidate) @Test CarCategoryIsCar() car <- Car() assert car.category() == "car" @Test AverageOfTwoAttempts() assert averageScore(150.0, 2.0) == 75.0 //EOF
Step 1: Generate Edge-Case Tests
Choose Code > Generate > Edge-Case Tests (whole file)…
Type 42 into Seed, so the result is the same every time, and quality.centre.tests into Test module, so the generated tests join dev/tests.ek9's own module rather than the service. Choose Generate.
Step 2: Apply, and the scaffolding
The preview lists what would be written - 7 tests ready to write. Nothing is on disk yet. Choose Apply to write them for real.
Choose Open generated file.
The file opens with a SCAFFOLDING header: the generator wrote the calls and the edge values, but did not decide what the right answer is. Every //AI: line marks one expectation to set FROM THE REQUIREMENT, never from what the code currently returns - running these and keeping whichever assertion happens to pass would cement today's behaviour, bugs included, forever.
Step 3: Run them
Run > Run Tests (⌘T) runs all ten - the three hand-written and the seven just generated. Choose Run Tests.
Two go red, for two different reasons. testFuzz_isMinimumAge_CandidateAge_0 called isMinimumAge with no age at all - an unset CandidateAge() - and the generator's naive guess expected a SET answer: a wrong guess, nothing to do with the requirement. testFuzz_isMinimumAge_CandidateAge17_1 called it with age 17 exactly, the Centre's own minimum, and expected true - and that one is real: the code reads age > MINIMUM_AGE, which is false at exactly 17.
Step 4: Decide each one
The requirement says nothing about a candidate with no age at all, so there is no reason to expect a set answer here. Select the live assert line for testFuzz_isMinimumAge_CandidateAge_0 and type the flip its own //AI: line offers: assert not isMinimumAge(age: CandidateAge())?. Saves when typed.
Open domain.ek9.
The requirement says 17 IS eligible, inclusive - that one is the code's own off-by-one, not the test's. Select isMinimumAge's return line and type it with >= in place of >: <- rtn as Boolean: age >= MINIMUM_AGE. Saves when typed.
Step 5: All green, and the report again
Run > Run Tests (⌘T) again - choose Run Tests.
Run > Quality Report (Shift+Cmd+R) writes the report again, now that isMinimumAge is tried at its own boundary. Choose Quality Report.
On the new index.html, isMinimumAge's own Input Variety rose - the boundary age is driven and asserted now, not just one interior value - and Verification's asserted ring rose with it: the same lines run as before, but more of what they could see is now actually checked.
The result
That is Generate Edge-Case Tests: it writes the calls and the edge values, and leaves every expectation marked //AI: for a person or an AI to set from the REQUIREMENT. One flip and one real code fix later, all ten pass - and the report's Input Variety and Verification both rise, because isMinimumAge is now tried at its own boundary, not just one interior age. Keeping whichever assertion happened to pass first would have cemented the off-by-one as correct, forever. AI assistants reach the same generator through ek9_generate_edge_case_tests.
Where this lesson comes from
- Answers -
ek9 -qand the IDE's Answers tab: 754. - For AI assistants - the MCP tool
ek9_generate_edge_case_tests.
In the EK9 reference: Test Generation in EK9, Testing in EK9













