torsdag 3. september 2009 Testing OO Patterns C#
Hvis du skummet gjennom min forrige post, TDD i praksis, så observerte du meg implementere registrering av ny bruker. Selv om det var en lang artikkel var det (minst) en ting jeg utelot der – det var nemlig ingen validering av input. Alle registreringer ville blitt en suksess uansett hva man tastet inn.
Validering kan gjøres på mange måter, men for meg er det for det første et eget ansvar som fortjener å bli implementert som egne tjenester/moduler, og for det andre er det et domene-ansvar, og bør ikke knyttes til infrastrukturen eller brukergrensesnittet (denne påstanden gjelder ikke for de mest banale valideringene, og igjen vil noen kunne kalle denne approachen for overkill).
Merk at dette er en konklusjon jeg mer eller mindre har funnet på av meg selv - en fremgangsmåte som gir mening for meg – så hvis du har noen kommentarer og innvendinger så er de som alltid hjertelig velkomne!
Jeg vil benytte anledningen til å implementere validering ved hjelp av specification pattern. Jeg har blogget om dette før, så jeg vil ikke ta med alle detaljene, men jeg vil spesielt vise hvilke tester jeg har skrevet for validering av input. Jeg skrev en og en test, og sørget for at hver test ble grønn før jeg gikk videre, men her har du hele testklassen i one go. Bruk litt tid på å lese gjennom dem..
103 [TestFixture]
104 public class When_validating_a_registration
105 {
106 Mock<UserRepository> mockRepository;
107 Specification<User> validator;
108 User user;
109
110 [SetUp]
111 public void SetUp()
112 {
113 mockRepository = new Mock<UserRepository>();
114 validator = new ValidRegistrationSpecification(mockRepository.Object);
115 // Default valid user registration
116 user = new User() { Username = "T-Man", Password = "abcd1234#" };
117 }
118 [Test]
119 public void Should_validate_a_strong_password() {
120 Assert.That(validator.IsSatisfiedBy(user));
121 }
122 [Test]
123 public void Should_not_validate_password_without_letters() {
124 user.Password = "12345678#";
125 Assert.IsFalse(validator.IsSatisfiedBy(user));
126 }
127 [Test]
128 public void Should_not_validate_password_without_digits()
129 {
130 user.Password = "abcdefgh#";
131 Assert.IsFalse(validator.IsSatisfiedBy(user));
132 }
133 [Test]
134 public void Should_not_validate_password_without_special_character()
135 {
136 user.Password = "abcd12345";
137 Assert.IsFalse(validator.IsSatisfiedBy(user));
138 }
139 [Test]
140 public void Should_not_validate_password_with_less_than_8_characters()
141 {
142 user.Password = "abc123#";
143 Assert.IsFalse(validator.IsSatisfiedBy(user));
144 }
145 [Test]
146 public void Should_not_validate_usernames_with_less_than_4_characters()
147 {
148 user.Username = "MrT";
149 Assert.IsFalse(validator.IsSatisfiedBy(user));
150 }
151 [Test]
152 public void Username_can_not_be_accepted_if_it_already_exist()
153 {
154 var mockRepository = new Mock<UserRepository>();
155 mockRepository
156 .Setup(rep => rep.Find(It.IsAny<Predicate<User>>()))
157 .Returns(new[] { new User() });
158 Assert.IsFalse(validator.IsSatisfiedBy(user));
159 }
160 }
Metodenavnene i testklassen gir en perfekt oppsummering av reglene for hva en som er en gyldig brukerregistrering. Et passord må inneholde bokstaver, tall, spesialtegn – totalt minst 8 stykker, og brukernavnet må være minst 4 tegn, og ikke eksistere fra før. Nå kunne jeg ha implementert denne valideringen med noen ganske få linjer kode, men jeg vil at designet skal reflektere kunnskapen om reglene, og at det skal være veldig enkelt å endre en og en regel, og å legge til eller fjerne regler. ValidRegistrationSpecification ser derfor slik ut:
78 public class ValidRegistrationSpecification : Specification<User>
79 {
80 private List<Specification<User>> _specifications;
81 public ValidRegistrationSpecification(UserRepository repository)
82 {
83 _specifications = new List<Specification<User>>();
84 _specifications.Add(new UsernameShouldBeAtLeastFourCharacters());
85 _specifications.Add(new UsernameMustBeAvailable(repository));
86 _specifications.Add(new PasswordMustHaveLetter());
87 _specifications.Add(new PasswordMustHaveDigit());
88 _specifications.Add(new PasswordMustHaveSpecialCharacter());
89 _specifications.Add(new PasswordMustHaveAtLeastEightCharacters());
90 }
91 public bool IsSatisfiedBy(User candidate)
92 {
93 try
94 {
95 foreach (var specification in _specifications)
96 if (!specification.IsSatisfiedBy(candidate))
97 return false;
98 }
99 catch (Exception) { }
100 return true;
101 }
102 }
Klassen arver fra Specification (for User) – et enkelt interface som bare inneholder én metode: IsSatisfiedBy(T candidate). Hvis IsSatifiedBy returnerer true for en gitt bruker, så kan registreringen gjennomføres.
ValidRegistrationSpecification er videre egentlig en liste med med mer detaljerte spesifikasjoner, én for hver test jeg har skrevet, og IsSatifiedBy spoler gjennom alle disse. La oss se nærmere på én av dem, som spesifiserer at et passord må inneholde bokstaver:
19 public class PasswordMustHaveLetter : Specification<User>
20 {
21 public bool IsSatisfiedBy(User candidate)
22 {
23 for (int i = 0; i < candidate.Password.Length; i++)
24 if (Char.IsLetter(candidate.Password[i]))
25 return true;
26 return false;
27 }
28 }
Du er kanskje ikke vandt til klasser på bare 10 linjer (inkludert klammene)? Det er ingenting galt i det skal jeg si deg.
Her følger den mest avanserte spesifikasjonen (med en Cyclomatic Complexity på hele 3) som kontrollerer at brukernavnet ikke er opptatt. Den er avhengig av en UserRepository som den benytter til å se om den finner en match på brukernavnet.
64 public class UsernameMustBeAvailable : Specification<User>
65 {
66 private UserRepository _repository;
67 public UsernameMustBeAvailable(UserRepository repository)
68 {
69 _repository = repository;
70 }
71 public bool IsSatisfiedBy(User candidate)
72 {
73 return _repository.Find((user)
74 => user.Username == candidate.Username)
75 .Count() == 0;
76 }
77 }
Jeg har ikke lest Eric Evans bok, så jeg vet ikke om denne løsningen er helt i tråd med Domain-Driven Design. Men jeg vet han spesifikt snakker om Specification pattern (excuse the pun). Og i sommer lærte Bellware meg at entiteter aldri skal referere domenetjenester (som repositories), så valideringen kunne ikke vært plassert i User-klassen.
For ordens skyld – jeg sier ikke at det jeg driver med er Domain-Driven Design. Jeg vil heller, som Bellware, kalle det DDD-light: Jeg bruker mønstrene og ideene fra DDD der det gir verdi og mening i det systemet jeg til enhver tid utvikler.
I neste blogpost skal jeg samle trådene fra utenfra-og-inn programmering, TDD og mocking i praksis og denne posten, og analysere designet jeg har kommet opp med.
Knagger: Bellware, DDD, Mocking, Moq, NUnit, Specification pattern, TDD, Utenfra-og-inn