This repository is a fork to be used during Software Craft Luxembourg Meetup.
Original concept has been presented by Arnaud Bailly and documented here.
- The kata
- 1) Validate a NIR (String)
- 2) Fight Primitive Obsession
- 3) Bulletproof your code with "Mutation-based Property-Driven Development"
- 4) Reflect
- "Solutions"
- To go further
- Resources
Write a system that can handle
NIR(simplified rules explained below)
In this hands-on session we will develop together a system that can handle NIR "French social security number" by using / experimenting the following practices:
Test-Driven Developmentto design a first version of our algorithm (with Example Based approach)Fight Primitive ObsessionwithParse Don't ValidateandMonadsType-Driven Developmentto "make impossible states unrepresentable"Property-Based Testingto drive our developmentMutation-Based Propertiesto refine our code and identify edge cases
At the end of this code kata you will have understood a different way of designing code that may / will inspire you for your day-to-day life.
Code is available in java and C# (#sharingiscaring)
NIR stands for "NumΓ©ro de sΓ©curitΓ© sociale en France" it is a unique id representing an individual composed by 15 characters.
Here are the simplified specifications you will use for this kata:
| Positions | Meaning | Possible values |
|---|---|---|
| 1 | Sex : 1 for men, 2 for women | 1 or 2 |
| 2, 3 | Last two digits of the year of birth (which gives the year to the nearest century) | From 00 to 99 |
| 4, 5 | Birth month | From 01 (January) to 12 (December) |
| 6, 7 | Department of birth | From 01 to 95, 99 for births abroad |
| 8, 9, 10 | Official code of the city of birth | From 001 to 999 |
| 11, 12, 13 | "Serial number": birth order number in the month and city | From 001 to 999 |
| 14, 15 | control key = complement to 97 of the number formed by the first 13 digits of the NIR modulo 97 | From 01 to 97 |
Here are some valid NIRs regarding those specifications:
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- 106099955391094
And here are some invalid ones:
- 2230 // too short
- 323115935012322 // incorrect sex
- 2ab115935012322 // incorrect year
- 223ab5935012322 // incorrect month
- 223145935012322 // incorrect month 2
- 223005935012322 // incorrect month 3
- 22311xx35012322 // incorrect department
- 223119635012322 // incorrect department 2
- 2231159zzz12322 // incorrect city
- 223115935012321 // incorrect control key
- Design a system that can validate if a given
Stringis a validNIRnumber:String -> Boolean - Use Test Driven Development to do so
The implementation has already been started using Test-Driven Development in the package / namespace primitive.obsession.
A test list has been created with our business experts after an Example Mapping workshop.
Here is the current status of this list:
Invalid NIRs
β
empty string
β
2230 // too short
β
323115935012322 // incorrect sex
β
2ab115935012322 // incorrect year
β
223ab5935012322 // incorrect month
β
223145935012322 // incorrect month 2
β
223005935012322 // incorrect month 3
β
22311xx35012322 // incorrect department
β
223119635012322 // incorrect department 2
β
2231159zzz12322 // incorrect city
β
2231159123zzz22 // incorrect serial number
β
2231159350123221 // too long
- 223115935012321 // incorrect control key
Valid NIRs
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- You have to finalize this test list:
class ValidateNIR {
public static Stream<Arguments> invalidNIRs() {
return Stream.of(
Arguments.of("", "empty string"),
Arguments.of("2230", "too short"),
Arguments.of("323115935012322", "incorrect sex"),
Arguments.of("2ab115935012322", "incorrect year"),
Arguments.of("223ab5935012322", "incorrect month"),
Arguments.of("223145935012322", "incorrect month 2"),
Arguments.of("223005935012322", "incorrect month 3"),
Arguments.of("22311xx35012322", "incorrect department"),
Arguments.of("223119635012322", "incorrect department 2"),
Arguments.of("2231159zzz12322", "incorrect city"),
Arguments.of("2231159350123221", "too long")
//Arguments.of("223115935012321", "incorrect key") -> work on it
);
}
...- Use
validNIRslist to ensure that valid NIRs are validated
What was the impact of using T.D.D and covering "non passing" tests on the design and implementation?
Know more about Primitive Obsession here
Let's apply "Parse Don't Validate" principle to fight "Primitive Obsession".
We will use Property Based Testing in this part of the kata to design our parser.
Our parsing function must respect the below property
for all (validNir)
parseNIR(nir.toString) == nir
With parse don't validate we want to make it impossible to represent an invalid NIR in our system. Our data structures need to be immutables.
Our parser may look like this: String -> Either<ParsingError, NIR>
- Start with a
parserthat always returnsRight[NIR]- Write a minimalist data structure first (empty one)
- Write a
positive propertytest checking valid NIR can be round-tripped- Round-tripping:
NIR -> String -> NIR- Assert that round-tripped
NIRequals originalNIR
- Assert that round-tripped
- To do so, you will have to create your own valid NIR generator
- Round-tripping:
- Write a
negative propertytest checkinginvalid NIRscan not be parsed- This is where mutations are introduced
- Each different mutation type representing some possible alteration of the
NIR - Generate invalid NIRs by introducing mutations in the valid ones
- Use the properties to guide your implementation
Inspired by Arnaud Bailly
- Add
vavr-testto do so
testImplementation("io.vavr:vavr-test:0.10.4")π΄ Specify the property
class NIRProperties {
private Arbitrary<NIR> validNIR = null;
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parse(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}π’ Make it pass.
- Generate the
NIRclass - Handle error with a data structure:
ParsingError
public record ParseError(String message) {
}
@EqualsAndHashCode
public class NIR {
public static Either<ParsingError, NIR> parse(String input) {
return right(new NIR());
}
@Override
public String toString() {
return "";
}
}
class NIRProperties {
private Arbitrary<NIR> validNIR = Arbitrary.of(new NIR());
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir") // describe the property
.forAll(validNIR) // pass an Arbitrary / Generator to generate valid NIRs
.suchThat(nir -> NIR.parse(nir.toString()).contains(nir)) // describe the Property predicate
.check()
.assertIsSatisfied();
}
}We will represent the NIR with proper immutable types like Sex, Department... carrying their own parsing logic and rules.
π΅ Create the Sex type
- We choose to use an
enumfor that - It is immutable by design
- We need to work on the
Stringrepresentation of it - Each data structure will contain its own parsing method
public enum Sex {
M(1), F(2);
private final int value;
Sex(int value) {
this.value = value;
}
public static Either<ParsingError, Sex> parseSex(char input) {
// vavr Pattern matching
return Match(input).of(
Case($('1'), right(M)),
Case($('2'), right(F)),
Case($(), left((new ParsingError("Not a valid sex"))))
);
}
@Override
public String toString() {
return "" + value;
}
}- Create a generator to be able to generate valid NIRs
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());- Extend
NIRwith the new created type
@EqualsAndHashCode
public class NIR {
private final Sex sex;
public NIR(Sex sex) {
this.sex = sex;
}
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIR::new);
}
@Override
public String toString() {
return sex.toString();
}
}
class NIRProperties {
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private final Arbitrary<NIR> validNIR =
sexGenerator.map(NIR::new)
.arbitrary();
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parseNIR(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}Like for the Sex type, we design the new type with its generator.
π΄ create a generator
private final Gen<Year> yearGenerator = Gen.choose(0, 99).map(Year::fromInt); // have a private constructor
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private final Arbitrary<NIR> validNIR =
sexGenerator
.map(NIR::new)
// use the yearGenerator here
.arbitrary();π’ To be able to use the yearGenerator, we need to have a context to be able to map into it.
It is a mutable data structure that we enrich with the result of each generator. We create a Builder class for it:
@With
@Getter
@AllArgsConstructor
public class NIRBuilder {
private final Sex sex;
private Year year;
public NIRBuilder(Sex sex) {
this.sex = sex;
}
}- We now adapt the
NIRPropertiesto use thisBuilder
class NIRProperties {
private final Random random = new Random();
private final Gen<Year> yearGenerator = Gen.choose(0, 99).map(Year::fromInt);
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private Arbitrary<NIR> validNIR =
sexGenerator
.map(NIRBuilder::new)
.map(builder -> builder.withYear(yearGenerator.apply(random)))
.map(x -> new NIR(x.getSex(), x.getYear()))
.arbitrary();
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parseNIR(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}- We now have to adapt the
NIRclass to handle theYearin its construct- We will use the same
Builderconstruct (in other languages we may usefor comprehensionorLinQfor example)
- We will use the same
@EqualsAndHashCode
@AllArgsConstructor
public class NIR {
private final Sex sex;
private final Year year;
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIRBuilder::new)
.flatMap(builder -> right(builder.withYear(new Year(1))))
.map(builder -> new NIR(builder.getSex(), builder.getYear()));
}
@Override
public String toString() {
return sex.toString() + year;
}
}π΅ We can now work on the Year type and its parser
@EqualsAndHashCode
@AllArgsConstructor
public class NIR {
private final Sex sex;
private final Year year;
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIRBuilder::new)
.flatMap(builder -> parseYear(input.substring(1, 3), builder))
.map(builder -> new NIR(builder.getSex(), builder.getYear()));
}
private static Either<ParsingError, NIRBuilder> parseYear(String input, NIRBuilder builder) {
return Year.parseYear(input)
.map(builder::withYear);
}
@Override
public String toString() {
return sex.toString() + year;
}
}
@EqualsAndHashCode
@ExtensionMethod(StringExtensions.class)
public class Year {
private final int value;
public Year(int value) {
this.value = value;
}
public static Either<ParsingError, Year> parseYear(String input) {
return input.toInt()
.filter(x -> x >= 0 && x <= 99)
.map(Year::new)
.toEither(new ParsingError("year should be between 0 and 99"));
}
public static Year fromInt(Integer x) {
return parseYear(x.toString())
.getOrElseThrow(() -> new IllegalArgumentException("Year"));
}
@Override
public String toString() {
return String.format("%02d", value);
}
}We can check Properties generation by printing the generated nirs:
214
241
240
182
138
294
280
252
158
265
213
225
275
Here are the iterations (You can see their details from the git history):

For now:
- It is impossible to represent a
NIRin an invalid state - We have a semantic that expresses the concepts behind
NIR
@Override
public String toString() {
return stringWithoutKey() + format("%02d", key());
}
// How the NIR is composed
private String stringWithoutKey() {
return sex.toString() + year + month + department + city + serialNumber;
}Is it enough designing our types like this?
Let's create a property that demonstrates that an invalid NIR can never be parsed.
We will generate a valid one and then mutate its string representation to create an invalid one.
For that we will create some mutators.
for all (validNir)
mutate(nir.toString) == left
Some example of mutations:
Sexmutant: a value greater than 2 for exampleKeymutant: change the key by using a number between 1 and 97 that does not respect the key definition
Which others mutators could you imagine? π«΅
private record Mutator(String name, Function1<NIR, Gen<String>> mutate){}
private static Mutator sexMutator = new Mutator("Sex mutator", nir ->
Gen.choose(3, 9)
.map(invalidSex -> invalidSex + nir.toString().substring(1))
);- Define the
propertyand use the mutator
class NIRMutatedProperties {
private static final Random random = new Random();
private record Mutator(String name, Function1<NIR, Gen<String>> func) {
public String mutate(NIR nir) {
return func.apply(nir).apply(random);
}
}
private static Mutator sexMutator = new Mutator("Sex mutator", nir ->
Gen.choose(3, 9)
.map(invalidSex -> invalidSex + nir.toString().substring(1))
);
private static Arbitrary<Mutator> mutators = Gen.choose(
sexMutator).arbitrary();
@Test
void invalidNIRCanNeverBeParsed() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR, mutators)
.suchThat(NIRMutatedProperties::canNotParseMutatedNIR)
.check()
.assertIsSatisfied();
}
private static boolean canNotParseMutatedNIR(NIR nir, Mutator mutator) {
return NIR.parseNIR(mutator.mutate(nir)).isLeft();
}
}What didi you learn from it?
β
Sex Mutator
β
Truncate the NIR
Year Mutator
Month Mutator
Department Mutator
City Mutator
Serial Number Mutator
Key Mutator
By using this approach you can mix T.D.D with Type Driven Development, Property-Based Testing and Mutations to design extremely robust code.
Indeed, it can help you quickly identify edge cases in your system.
What did you learn today? How could you apply this learning?
Read more about it here
Proposal of solutions are available in the solution directory:
C#withxUnit|LanguageExt|FsCheckjavawithjUnit|vavr|vavr-testscala 3withscalatest|scalacheckF#withxUnit|FsCheckkotlinwithkotest
A step-by-step guide in java is available here
I have created another kata to practice those ideas called snafu it is available here.




