Pewnie każdy na swojej ścieżce programistycznej spotkał się z klasą abstrakcyjną. Wrzucamy tam kod, który zdaje się być domyślną implementacją pewnej grupy klas i szkoda nam kopiować tego zachowania do każdej z nich osobna. Skoro wszystkie zachowują się podobny sposób, czasem tylko dodając coś od siebie, to warto wykorzystać dziedziczenie i napisać mniej (DRY). Czyli prawdopodobny wydaje się taki scenariusz:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
max-height: 500px;
overflow: auto;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: namespace AbstractTesting
2: {
3: public abstract class AbstractClass
4: {
5: public virtual int Method(int input)
6: {
7: switch (input)
8: {
9: case 1:
10: return 1;
11: case 2:
12: return 4;
13: case 3:
14: return 8;
15: default:
16: return 0;
17: }
18: }
19: }
20:
21: public class ConcreteClass1 : AbstractClass
22: {
23: public override int Method(int input)
24: {
25: switch (input)
26: {
27: case 1:
28: return -1;
29: default:
30: return base.Method(input);
31: }
32:
33: }
34: }
35:
36: public class ConcreteClass2 : AbstractClass
37: {
38: public override int Method(int i)
39: {
40: switch (i)
41: {
42: case 4:
43: return 16;
44: default:
45: return base.Method(i);
46:
47: }
48: }
49: }
50:
51: public class ConcreteClass3 : AbstractClass
52: {
53: }
54:
55: public class ConcreteClass4 : AbstractClass
56: {
57: public override int Method(int i)
58: {
59: switch (i)
60: {
61: case 1:
62: return -1;
63: case 2:
64: return -2;
65: case 3:
66: return -3;
67: default:
68: return base.Method(i);
69: }
70: }
71: }
72:
73: public class ConcreteClass5 : AbstractClass
74: {
75: public override int Method(int i)
76: {
77: switch (i)
78: {
79: case 1:
80: return -1;
81: case 2:
82: return -4;
83: case 3:
84: return -8;
85: default:
86: return 1;
87: }
88: }
89: }
90: }
Celowo pomijam dodatkowe metody w klasie bazowej i klasach pochodnych, nie na tym chce się tutaj skupić.
Teraz można zadać sobie pytanie, jak mam to przetestować? Przynajmniej ja je sobie zadawałem. Czy testować w każdej klasie tylko to co wnosi ona nowego, a dla klasy klasy bazowej napisać specjalnego mocka, który umożliwi stworzenie jej instancji, a następnie przetestowanie wspólnego kodu. Czy też pominąć w testach klasę bazową, ale dla każdej z klas dziedziczących pisać powtarzający się test, sprawdzający wszystkie warunki switcha? Jest też trzecie rozwiązanie tego problemu, sugerując się podpowiedziami kolegów z pracy, trzeba być wystarczająco odważnym, wrzucić na produkcję i zobaczyć czy zabangla. Ale tego przykładu nie będę tutaj analizować.
Zamiast rozwodzić się nad tym, co jest lepsze, a co gorsze, sprawdzimy jak to wychodzi w praniu. Pierwsze podejście, napisać mniej testów:
1: [TestFixture]
2: public class AbstractTestClass
3: {
4: [Test]
5: public void t0_1()
6: {
7: var sut = new MockAbstractClass();
8: Assert.AreEqual(1, sut.Method(1));
9: }
10:
11: [Test]
12: public void t1_4()
13: {
14: var sut = new MockAbstractClass();
15: Assert.AreEqual(4, sut.Method(2));
16: }
17:
18: [Test]
19: public void t3_8()
20: {
21: var sut = new MockAbstractClass();
22: Assert.AreEqual(8, sut.Method(3));
23: }
24:
25: [Test]
26: public void t4_0()
27: {
28: var sut = new MockAbstractClass();
29: Assert.AreEqual(0, sut.Method(4));
30: }
31: }
32:
33: public class MockAbstractClass : AbstractClass
34: {
35: }
36:
37: [TestFixture]
38: public class TestC1
39: {
40: [Test]
41: public void t0_1()
42: {
43: var sut = new ConcreteClass1();
44: Assert.AreEqual(-1, sut.Method(1));
45: }
46: }
47:
48: [TestFixture]
49: public class TestC2
50: {
51: [Test]
52: public void t0_16()
53: {
54: var sut = new ConcreteClass2();
55: Assert.AreEqual(16, sut.Method(4));
56: }
57: }
58:
59: [TestFixture]
60: public class TestC4
61: {
62: [Test]
63: public void t1_m1()
64: {
65: var sut = new ConcreteClass4();
66: Assert.AreEqual(-1, sut.Method(1));
67: }
68:
69: [Test]
70: public void t2_m2()
71: {
72: var sut = new ConcreteClass4();
73: Assert.AreEqual(-2, sut.Method(2));
74: }
75:
76: [Test]
77: public void t3_m3()
78: {
79: var sut = new ConcreteClass4();
80: Assert.AreEqual(-3, sut.Method(3));
81: }
82:
83: [Test]
84: public void t4_0()
85: {
86: var sut = new ConcreteClass4();
87: Assert.AreEqual(0, sut.Method(4));
88: }
89: }
90:
91: [TestFixture]
92: public class TestC5
93: {
94: [Test]
95: public void t1_m1()
96: {
97: var sut = new ConcreteClass5();
98: Assert.AreEqual(-1, sut.Method(1));
99: }
100:
101: [Test]
102: public void t2_m2()
103: {
104: var sut = new ConcreteClass5();
105: Assert.AreEqual(-4, sut.Method(2));
106: }
107:
108: [Test]
109: public void t3_m3()
110: {
111: var sut = new ConcreteClass5();
112: Assert.AreEqual(-8, sut.Method(3));
113: }
114:
115: [Test]
116: public void t4_0()
117: {
118: var sut = new ConcreteClass5();
119: Assert.AreEqual(1, sut.Method(16));
120: }
121: }
Pokrycie kodu powinno być 100%. Wszystkie test powinny przejść na zielono. Skoro wszystko działa dobrze, to po co się zastanawiać na innym rozwiązaniem? A to dlatego, drogie dzieciaczki, że jeśli wprowadzicie teraz jakąś zmianę w klasie Abstract, to wszystkie testy klas pochodnych nadal będą się świecić na zielono. Pomimo tego, że zmieni się ich zachowanie. W ten sposób nie dowiecie się (z testów), że np. klasa Concret3 od teraz zachowuje się zupełnie inaczej, inne korzystające z domyślnego zachowania też oszalały. Ktoś może powiedzieć, że to poprawne zachowanie, skoro dziedziczysz po czymś/kimś i zdajesz się na jego domyślną implementacje to zmieniasz się razem z nią jak chorągiewka na wietrze. Tylko czy o to chodzi? Czy klasy nie powinny mieć własnego rozumku? Klient korzysta z implementacji klasy 1,2,3,4 czy 5 a nie klasy podstawowej. W takiej sytuacji każda klasa, a w zasadzie test dla każdej z klas powinien wykryć zmiany w jej zachowaniu, poinformować o tym, że zostało ono zmienione i od teraz nie spełnia wcześniej przyjętego rozumowania. Moim zdaniem tak powinny wyglądać testy:
1: namespace AbstractTesting
2: {
3: [TestFixture]
4: public class ConcreteClass1Test
5: {
6: [Test]
7: public void T001_M1()
8: {
9: var sut = new ConcreteClass1();
10:
11: var result = sut.Method(1);
12:
13: Assert.AreEqual(-1, result);
14: }
15:
16: [Test]
17: public void T002_M2()
18: {
19: var sut = new ConcreteClass1();
20:
21: var result = sut.Method(2);
22:
23: Assert.AreEqual(4, result);
24: }
25:
26: [Test]
27: public void T003_M3()
28: {
29: var sut = new ConcreteClass1();
30:
31: var result = sut.Method(3);
32:
33: Assert.AreEqual(8, result);
34: }
35:
36: [Test]
37: public void T004_M155()
38: {
39: var sut = new ConcreteClass1();
40:
41: var result = sut.Method(155);
42:
43: Assert.AreEqual(0, result);
44: }
45: }
46:
47: [TestFixture]
48: public class ConcreteClass2Test
49: {
50: [Test]
51: public void T001_M1()
52: {
53: var sut = new ConcreteClass2();
54:
55: var result = sut.Method(1);
56:
57: Assert.AreEqual(1, result);
58: }
59:
60: [Test]
61: public void T002_M2()
62: {
63: var sut = new ConcreteClass2();
64:
65: var result = sut.Method(2);
66:
67: Assert.AreEqual(4, result);
68: }
69:
70: [Test]
71: public void T003_M3()
72: {
73: var sut = new ConcreteClass2();
74:
75: var result = sut.Method(3);
76:
77: Assert.AreEqual(8, result);
78: }
79:
80: [Test]
81: public void T004_M4()
82: {
83: var sut = new ConcreteClass2();
84:
85: var result = sut.Method(4);
86:
87: Assert.AreEqual(16, result);
88: }
89:
90: [Test]
91: public void T005_M55()
92: {
93: var sut = new ConcreteClass2();
94:
95: var result = sut.Method(55);
96:
97: Assert.AreEqual(0, result);
98: }
99: }
100:
101: [TestFixture]
102: public class ConcreteClass3Test
103: {
104: [Test]
105: public void T001_M1()
106: {
107: var sut = new ConcreteClass3();
108:
109: var result = sut.Method(1);
110:
111: Assert.AreEqual(1, result);
112: }
113:
114: [Test]
115: public void T002_M2()
116: {
117: var sut = new ConcreteClass3();
118:
119: var result = sut.Method(2);
120:
121: Assert.AreEqual(4, result);
122: }
123:
124: [Test]
125: public void T003_M3()
126: {
127: var sut = new ConcreteClass3();
128:
129: var result = sut.Method(3);
130:
131: Assert.AreEqual(8, result);
132: }
133:
134: [Test]
135: public void T004_M55()
136: {
137: var sut = new ConcreteClass3();
138:
139: var result = sut.Method(55);
140:
141: Assert.AreEqual(0, result);
142: }
143: }
144:
145: [TestFixture]
146: public class ConcreteClass4Test
147: {
148: [Test]
149: public void T001_M1()
150: {
151: var sut = new ConcreteClass4();
152:
153: var result = sut.Method(1);
154:
155: Assert.AreEqual(-1, result);
156: }
157:
158: [Test]
159: public void T002_M2()
160: {
161: var sut = new ConcreteClass4();
162:
163: var result = sut.Method(2);
164:
165: Assert.AreEqual(-2, result);
166: }
167:
168: [Test]
169: public void T003_M3()
170: {
171: var sut = new ConcreteClass4();
172:
173: var result = sut.Method(3);
174:
175: Assert.AreEqual(-3, result);
176: }
177:
178: [Test]
179: public void T004_M55()
180: {
181: var sut = new ConcreteClass4();
182:
183: var result = sut.Method(55);
184:
185: Assert.AreEqual(0, result);
186: }
187: }
188:
189: [TestFixture]
190: public class ConcreteClass5Test
191: {
192: [Test]
193: public void T001_M1()
194: {
195: var sut = new ConcreteClass5();
196:
197: var result = sut.Method(1);
198:
199: Assert.AreEqual(-1, result);
200: }
201:
202: [Test]
203: public void T002_M2()
204: {
205: var sut = new ConcreteClass5();
206:
207: var result = sut.Method(2);
208:
209: Assert.AreEqual(-4, result);
210: }
211:
212: [Test]
213: public void T003_M3()
214: {
215: var sut = new ConcreteClass5();
216:
217: var result = sut.Method(3);
218:
219: Assert.AreEqual(-8, result);
220: }
221:
222: [Test]
223: public void T004_M55()
224: {
225: var sut = new ConcreteClass5();
226:
227: var result = sut.Method(55);
228:
229: Assert.AreEqual(1, result);
230: }
231: }
232: }
W tym przypadku gdy zmieni się zachowanie klasy podstawowej, wszystkie testy klas korzystających z domyślnej implementacji, automatycznie zgłoszą niezadowolenie ze zmian.
Nic za darmo, w drugim przypadku trzeba napisać więcej testów, a czasem nawet go powtórzyć. Oczywiście można też pozbyć się tego switcha z kodu, albo wyjechać do Niemiec i zbierać ogórki – byle by tylko nie pisać testów. Tutaj miałem na celu pokazanie tego, jak zachowują się oba rozwiązania i które z nich, moim zdaniem jest tym słuszniejszym.
Jak zwykle pozostaje otwarty na krytykę i zastrzegam sobie prawo do popełniania błędów.