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 Development
to design a first version of our algorithm (with Example Based approach)Fight Primitive Obsession
withParse Don't Validate
andMonads
Type-Driven Development
to "make impossible states unrepresentable"Property-Based Testing
to drive our developmentMutation-Based Properties
to 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
String
is a validNIR
number: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
validNIRs
list 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
parser
that always returnsRight[NIR]
- Write a minimalist data structure first (empty one)
- Write a
positive property
test checking valid NIR can be round-tripped- Round-tripping:
NIR -> String -> NIR
- Assert that round-tripped
NIR
equals originalNIR
- Assert that round-tripped
- To do so, you will have to create your own valid NIR generator
- Round-tripping:
- Write a
negative property
test checkinginvalid NIRs
can 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-test
to 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
NIR
class - 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
enum
for that - It is immutable by design
- We need to work on the
String
representation 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
NIR
with 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
NIRProperties
to 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
NIR
class to handle theYear
in its construct- We will use the same
Builder
construct (in other languages we may usefor comprehension
orLinQ
for 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
NIR
in 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:
Sex
mutant: a value greater than 2 for exampleKey
mutant: 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
property
and 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
|FsCheck
java
withjUnit
|vavr
|vavr-test
scala 3
withscalatest
|scalacheck
F#
withxUnit
|FsCheck
kotlin
withkotest
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.