-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest-di-unità.html
827 lines (704 loc) · 63.1 KB
/
test-di-unità.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
<!DOCTYPE html>
<meta charset=utf-8>
<title>Test di unità - Immersione in Python 3</title>
<!--[if IE]><script src=j/html5.js></script><![endif]-->
<link rel=stylesheet href=dip3.css>
<style>
body{counter-reset:h1 9}
</style>
<link rel=stylesheet media='only screen and (max-device-width: 480px)' href=mobile.css>
<link rel=stylesheet media=print href=print.css>
<meta name=viewport content='initial-scale=1.0'>
<form action=http://www.google.com/cse><div><input type=hidden name=cx value=014021643941856155761:l5eihuescdw><input type=hidden name=ie value=UTF-8> <input type=search name=q size=25 placeholder="powered by Google™"> <input type=submit name=root value=Search></div></form>
<p>Voi siete qui: <a href=index.html>Inizio</a> <span class=u>‣</span> <a href=indice.html#test-di-unità>Immersione in Python 3</a> <span class=u>‣</span>
<p id=level>Livello di difficoltà: <span class=u title=intermedio>♦♦♦♢♢</span>
<h1>Test di unità</h1>
<blockquote class=q>
<p><span class=u>❝</span> La convinzione non è la prova della certezza. Siamo stati assolutamente sicuri di molte cose che non erano così. <span class=u>❞</span><br>— <a href=http://en.wikiquote.org/wiki/Oliver_Wendell_Holmes,_Jr.>Oliver Wendell Holmes, Jr.</a>
</blockquote>
<p id=toc>
<h2 id=divingin>Immersione! (Oppure no?)</h2>
<p class=f>Mentre i ragazzini di oggi vengono rovinati da computer veloci e da stravaganti linguaggi “dinamici” che li inducono a scrivere codice come prima cosa, poi a consegnare il software, e solo dopo a effettuare il debug (sempre che questa attività di correzione degli errori venga svolta), ricordo che ai miei tempi eravamo obbligati a una certa disciplina. Scrivevamo i programmi a <em>mano</em>, su <em>carta</em>, e il computer li leggeva da <em>schede perforate</em>. E tutto questo <em>ci piaceva</em>!
<p>In questo capitolo scriverete e farete il debug di un insieme di funzioni di utilità per convertire numeri interi in numeri romani e viceversa. Avete visto le meccaniche per la costruzione e la validazione dei numeri romani nel <a href=espressioni-regolari.html#romannumerals>“Caso di studio: numeri romani”</a>. Ora fate un passo indietro e considerate come potremmo fare per trasformare quel programma in un’utilità a due vie.
<p><a href=espressioni-regolari.html#romannumerals>Le regole per i numeri romani</a> ci hanno condotto a una serie di interessanti osservazioni.
<ol>
<li>C’è un solo modo corretto di rappresentare un particolare numero come numero romano.
<li>Anche l’inverso è vero: se una stringa di caratteri è un numero romano valido, rappresenta un solo numero (cioè, può essere interpretata in un solo modo).
<li>C’è un intervallo limitato di numeri esprimibili come numeri romani che nello specifico va da <code>1</code> a <code>3999</code>. I romani avevano diversi modi di esprimere numeri più grandi, per esempio tracciando una barra sopra un numero per dire che il suo valore normale doveva essere moltiplicato per 1000. Per gli scopi di questo capitolo, conveniamo che i numeri romani vadano da <code>1</code> a <code>3999</code>.
<li>Non c’è alcun modo di rappresentare 0 nei numeri romani.
<li>Non c’è alcun modo di rappresentare numeri negativi nei numeri romani.
<li>Non c’è alcun modo di rappresentare frazioni o numeri non interi nei numeri romani.
</ol>
<p>Cominciamo col tracciare ciò che un modulo <code>roman.py</code> dovrebbe fare. Il modulo avrà due funzioni principali, <code>to_roman()</code> e <code>from_roman()</code>. La funzione <code>to_roman()</code> dovrà prendere un intero da <code>1</code> a <code>3999</code> e restituirne la rappresentazione come numero romano sotto forma di stringa…
<p>Fermiamoci subito qui. Ora facciamo qualcosa di un poco inaspettato: scriviamo un test per controllare che la funzione <code>to_roman()</code> faccia quello che volete. Avete letto bene: state per scrivere codice che collauda codice che non avete ancora scritto.
<p>Questa pratica si chiama <i>sviluppo guidato dai test</i> (in inglese, <i>test-driven development</i> o <abbr>TDD</abbr>). L’insieme di due funzioni di conversione — <code>to_roman()</code> e più tardi <code>from_roman()</code> — può essere implementato e collaudato come una singola unità, separata da qualsiasi programma più grande che le importi. Python possiede un framework per il collaudo di unità, un modulo propriamente chiamato <code>unittest</code>.
<p>Il collaudo di unità è una parte importante di una strategia di sviluppo complessivamente centrata sui test. Se scrivete i test di unità, è importante che li scriviate presto e che li manteniate aggiornati man mano che il codice e i requisiti cambiano. Molte persone sostengono che i test vadano scritti prima di scrivere il codice che collaudano e questo è lo stile di sviluppo che mostrerò in questo capitolo. Ma i test di unità portano benefici a prescindere dal momento in cui li scrivete.
<ul>
<li>Prima di scrivere codice, scrivere i test di unità vi obbliga a dettagliare i vostri requisiti in modo utile.
<li>Mentre scrivete codice, i test di unità vi evitano di scriverne troppo. Quando tutti i test passano, la funzione è completa.
<li>Quando applicate il refactoring al codice, vi assicurano che la nuova versione si comporta allo stesso modo della vecchia versione.
<li>Durante la manutenzione del codice, i test vi aiutano a pararvi il culo quando qualcuno arriva urlando che il vostro ultimo cambiamento ha guastato il suo vecchio codice (“Ma <em>signore</em>, tutti i test di unità passavano quando l’ho inserito nel nostro sistema di controllo di revisione…”)
<li>Quando scrivete codice in gruppo, avere una serie di test completa riduce drammaticamente la probabilità che il vostro codice vada a guastare il codice di qualcun altro, perché prima potete lanciare i loro test di unità. (Ho visto questo tipo di cose nelle maratone di programmazione. Un gruppo si divide i compiti, ognuno prende le specifiche per il proprio compito, scrive i relativi test di unità e poi li condivide con il resto del gruppo. In questo modo, nessuno si spinge talmente lontano fino a sviluppare codice che non si interfaccia bene con quello degli altri.)
</ul>
<p class=a>⁂
<h2 id=romantest1>Una singola domanda</h2>
<aside>Ogni test è un’isola.</aside>
<p>Un test risponde a una singola domanda sul codice che sta collaudando. Un test dovrebbe essere in grado di…
<ul>
<li>…eseguire completamente da solo, senza alcun intervento umano. Il collaudo di unità ha a che fare con l’automazione.
<li>…determinare da solo se la funzione che sta collaudando ha avuto successo oppure è fallita, senza una persona che interpreti i risultati.
<li>…eseguire in isolamento, separato da qualsiasi altro test (anche se collaudano la stessa funzione). Ogni test è un’isola.
</ul>
<p>Detto questo, costruiamo un test per il primo requisito.
<ol>
<li>La funzione <code>to_roman()</code> dovrebbe restituire la rappresentazione come numero romano di tutti gli interi da <code>1</code> a <code>3999</code>.
</ol>
<p>Non è immediatamente ovvio come questo codice faccia… be’, <em>alcunché</em>. Definisce una classe che non ha alcun metodo <code>__init__()</code>. La classe <em>ha</em> un altro metodo, ma non viene mai chiamato. L’intero programma ha un blocco <code>__main__</code>, che però non fa riferimento alla classe o al suo metodo. Ma fa qualcosa, prometto.
<p class=d>[<a href=esempi/romantest1.py>scarica <code>romantest1.py</code></a>]
<pre class=pp><code>import roman1
import unittest
<a>class KnownValues(unittest.TestCase): <span class=u>①</span></a>
known_values = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
<a> (3999, 'MMMCMXCIX')) <span class=u>②</span></a>
<a> def test_to_roman_known_values(self): <span class=u>③</span></a>
'''to_roman dovrebbe dare un risultato noto con un ingresso noto'''
for integer, numeral in self.known_values:
<a> result = roman1.to_roman(integer) <span class=u>④</span></a>
<a> self.assertEqual(numeral, result) <span class=u>⑤</span></a>
if __name__ == '__main__':
unittest.main()</code></pre>
<ol>
<li>Per scrivere un test, come prima cosa estendete la classe <code>TestCase</code> del modulo <code>unittest</code>. Questa classe mette a disposizione molti metodi utili che potete usare nel vostro test per esprimere specifiche condizioni di collaudo.
<li>Questa è una lista di coppie interi/romani che ho verificato manualmente. Include i dieci numeri più bassi, il numero più alto, ogni numero che si traduce in un numero romano con un singolo carattere e una serie di esempi casuali di altri numeri validi. Non dovete collaudare ogni possibile ingresso, ma dovreste provare a collaudare tutti gli ovvi casi limite.
<li>Ogni test individuale ha il proprio metodo. Un metodo di test non prende parametri, non restituisce alcun valore e deve avere un nome che comincia con le quattro lettere <code>test</code>. Se il metodo di test ritorna normalmente senza sollevare un’eccezione, il test si considera passato; se il metodo solleva un’eccezione, il test si considera fallito.
<li>Qui potete chiamare la funzione <code>to_roman()</code>. (Be’, la funzione non è ancora stata scritta, ma una volta che lo sarà questa è la riga che la chiamerà.) Notate che avete definito la <abbr>API</abbr> per la funzione <code>to_roman()</code>: deve prendere un intero (il numero da convertire) e restituire una stringa (la rappresentazione come numero romano). Se la <abbr>API</abbr> è differente, il test si considera fallito. Notate anche che state intenzionalmente evitando di catturare alcuna eccezione quando chiamate <code>to_roman()</code>. La funzione non dovrebbe sollevare alcuna eccezione quando la chiamate con un ingresso valido, e questi valori di ingresso sono tutti validi. Se <code>to_roman()</code> solleva un’eccezione, questo test si considera fallito.
<li>Supponendo che la funzione <code>to_roman()</code> sia stata definita correttamente, invocata correttamente, abbia completato con successo la propria esecuzione e abbia restituito un valore, l’ultimo passo è controllare se ha restituito il valore <em>corretto</em>. Questa è una necessità talmente comune che la classe <code>TestCase</code> vi fornisce il metodo <code>assertEqual()</code> per controllare se due valori sono uguali. Se il risultato restituito da <code>to_roman()</code> (<var>result</var>) non corrisponde al valore noto che vi stavate aspettando (<var>numeral</var>), <code>assertEqual()</code> solleverà un’eccezione e il test fallirà. Se i due valori sono uguali, <code>assertEqual()</code> non farà nulla. Se tutti i valori restituiti da <code>to_roman()</code> corrispondono ai valori che vi aspettate, <code>assertEqual()</code> non solleverà mai alcuna eccezione, così <code>test_to_roman_known_values()</code> concluderà normalmente la propria esecuzione, il che significa che <code>to_roman()</code> avrà passato questo test.
</ol>
<aside>Scrivete un test che fallisce, poi programmate fino a quando ha successo.</aside>
<p>Ora che avete scritto un test, potete cominciare a implementare la funzione <code>to_roman()</code>. Prima di tutto, dovreste creare lo scheletro vuoto della funzione e verificare che il test fallisca. Se il test ha successo prima che abbiate scritto il codice della funzione, i vostri test non stanno collaudando il vostro codice proprio per niente! Il collaudo di unità è un ballo: i test guidano, il codice segue. Scrivete un test che fallisce, poi programmate fino a quando ha successo.
<pre class=pp><code># roman1.py
def to_roman(n):
'''converte un intero in un numero romano'''
<a> pass <span class=u>①</span></a></code></pre>
<ol>
<li>A questo punto, volete definire la <abbr>API</abbr> della funzione <code>to_roman()</code>, ma non volete ancora scriverne il codice. (Il vostro test deve prima fallire.) Per creare lo scheletro della funzione, usate la parola riservata <code>pass</code> di Python, che non fa proprio nulla.
</ol>
<p>Per lanciare i test, eseguite <code>romantest1.py</code> dalla linea di comando. Se chiamate lo script con l’opzione <code>-v</code>, le informazioni visualizzate saranno più verbose in modo che possiate vedere esattamente cosa succede quando ogni test viene eseguito. Con ogni probabilità, il risultato della vostra esecuzione dovrebbe somigliare a questo:
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest1.py -v</kbd>
<a><samp>test_to_roman_known_values (__main__.KnownValues)</samp> <span class=u>①</span></a>
<a><samp>to_roman dovrebbe dare un risultato noto con un ingresso noto ... FAIL</samp> <span class=u>②</span></a>
<samp>
======================================================================
FAIL: to_roman dovrebbe dare un risultato noto con un ingresso noto
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest1.py", line 73, in test_to_roman_known_values
self.assertEqual(numeral, result)
<a>AssertionError: 'I' != None <span class=u>③</span></a>
----------------------------------------------------------------------
<a>Ran 1 test in 0.016s <span class=u>④</span></a>
<a>FAILED (failures=1) <span class=u>⑤</span></a></samp></pre>
<ol>
<li>Lanciare lo script esegue <code>unittest.main()</code> che a sua volta esegue tutti i test. Ogni test è un metodo all’interno di una classe in <code>romantest.py</code>. Queste classi di test non devono essere obbligatoriamente organizzate in alcun modo: possono tutte contenere un singolo metodo di test, oppure potete avere una classe che contiene più metodi di test. L’unico requisito è che ogni classe di test erediti da <code>unittest.TestCase</code>.
<li>Per ogni test, il modulo <code>unittest</code> mostrerà la <code>docstring</code> del metodo e un risultato di successo o fallimento per quel test. Come ci aspettavamo, questo test fallisce.
<li>Per ogni test fallito, <code>unittest</code> visualizzerà le informazioni della traccia dello stack di esecuzione che mostrano esattamente cos’è successo. In questo caso, l’invocazione di <code>assertEqual()</code> ha sollevato un’eccezione di tipo <code>AssertionError</code> perché si aspettava che <code>to_roman(1)</code> restituisse <code>'I'</code>, ma non lo ha fatto. (Dato che non c’era nessuna istruzione esplicita di <code>return</code>, la funzione ha restituito <code>None</code>, il valore nullo di Python.)
<li>Dopo i dettagli di ogni test, <code>unittest</code> visualizza un riepilogo di quanti test sono stati eseguiti e quanto tempo hanno impiegato.
<li>Nell’insieme, l’esecuzione del collaudo è fallita perché almeno un test non è passato. Quando un test non passa, <code>unittest</code> distingue tra fallimenti ed errori. Un fallimento corrisponde all’invocazione di un metodo di asserzione, come <code>assertEqual()</code> o <code>assertRaises()</code>, che fallisce perché la condizione asserita non è vera o l’eccezione attesa non è stata sollevata. Un errore corrisponde a qualsiasi altro tipo di eccezione sollevata dal codice che state collaudando o dal test di unità stesso.
</ol>
<p><em>Ora</em> finalmente potete scrivere la funzione <code>to_roman()</code>.
<p class=d>[<a href=esempi/roman1.py>scarica <code>roman1.py</code></a>]
<pre class=pp><code>roman_numeral_map = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
<a> ('I', 1)) <span class=u>①</span></a>
def to_roman(n):
'''converte un intero in un numero romano'''
result = ''
for numeral, integer in roman_numeral_map:
<a> while n >= integer: <span class=u>②</span></a>
result += numeral
n -= integer
return result</code></pre>
<ol>
<li><var>roman_numeral_map</var> è una tupla di tuple che definisce tre cose: la rappresentazione a caratteri dei numeri romani di base; l’ordine dei numeri romani (discendente secondo il valore, da <code>M</code> fino a <code>I</code>); il valore di ogni numero romano. Ogni tupla interna rappresenta una coppia <code>(<var>numero</var>, <var>valore</var>)</code>. Non ci sono solo i numeri romani con un singolo carattere; sono definite anche coppie di due caratteri come <code>CM</code> (“un centinaio meno un migliaio”). Questo rende più semplice il codice della funzione <code>to_roman()</code>.
<li>Ed ecco dove la ricca struttura dati di <var>roman_numeral_map</var> vi ricompensa, perché non avete bisogno di alcuna logica speciale per gestire la regola di sottrazione. Per convertire in numeri romani, dovete semplicemente iterare attraverso <var>roman_numeral_map</var> cercando il più grande valore intero minore o uguale al numero in ingresso. Una volta trovato, aggiungete la rappresentazione del numero romano alla fine del risultato, sottraete il corrispondente valore intero dal numero in ingresso, insaponate, sciacquate, ripetete.
</ol>
<p>Se non vi è ancora chiaro come lavora la funzione <code>to_roman()</code>, aggiungete una chiamata a <code>print()</code> alla fine del ciclo <code>while</code>:
<pre class='nd pp'><code>
while n >= integer:
result += numeral
n -= integer
print('sottraggo {0} dal numero in ingresso, aggiungo {1} al risultato'.format(integer, numeral))</code></pre>
<p>Con l’istruzione <code>print()</code> di debug, il risultato somiglia al seguente:
<pre class='nd screen'>
<samp class=p>>>> </samp><kbd class=pp>import roman1</kbd>
<samp class=p>>>> </samp><kbd class=pp>roman1.to_roman(1424)</kbd>
<samp>sottraggo 1000 dal numero in ingresso, aggiungo M al risultato
sottraggo 400 dal numero in ingresso, aggiungo CD al risultato
sottraggo 10 dal numero in ingresso, aggiungo X al risultato
sottraggo 10 dal numero in ingresso, aggiungo X al risultato
sottraggo 4 dal numero in ingresso, aggiungo IV al risultato
'MCDXXIV'</samp></pre>
<p>Quindi la funzione <code>to_roman()</code> sembra funzionare, almeno in questo controllo manuale. Ma passerà il test che avete scritto?
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest1.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
<a>to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok <span class=u>①</span></a>
----------------------------------------------------------------------
Ran 1 test in 0.016s
OK</samp></pre>
<ol>
<li>Urrà! La funzione <code>to_roman()</code> passa il test per i “valori noti”. Certo il test non è completo, ma mette alla prova la funzione con una varietà di ingressi, compresi interi che producono ogni numero romano con un singolo carattere, l’intero più grande possibile (<code>3999</code>) e l’intero che produce il numero romano più lungo (<code>3888</code>). A questo punto, potete essere ragionevolmente fiduciosi che la funzione converta con successo ogni ingresso valido che potreste darle.
</ol>
<p>Ingresso “valido”? Hmm. E se l’ingresso non fosse valido?
<p class=a>⁂
<h2 id=romantest2>“Fermati e prendi fuoco”</h2>
<aside>Il modo che Python ha di fermarsi e prendere fuoco è sollevare un’eccezione.</aside>
<p>Non è sufficiente verificare che una funzione abbia successo quando le viene dato un ingresso valido; dovete anche verificare che fallisca quando le viene dato un ingresso non valido. E non un qualsiasi tipo di fallimento; la funzione deve fallire nel modo che vi aspettate.
<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>import roman1</kbd>
<samp class=p>>>> </samp><kbd class=pp>roman1.to_roman(4000)</kbd>
<samp class=pp>'MMMM'</samp>
<samp class=p>>>> </samp><kbd class=pp>roman1.to_roman(5000)</kbd>
<samp class=pp>'MMMMM'</samp>
<a><samp class=p>>>> </samp><kbd class=pp>roman1.to_roman(9000)</kbd> <span class=u>①</span></a>
<samp class=pp>'MMMMMMMMM'</samp></pre>
<ol>
<li>Questo non è assolutamente ciò che desiderate — quello non è nemmeno un numero romano valido! In effetti, ognuno di questi numeri è fuori dall’intervallo degli ingressi accettabili, ma la funzione restituisce comunque un finto valore. Restituire silenziosamente valori sbagliati è <em>maaaaaaale</em>; se un programma deve fallire, è molto meglio che fallisca velocemente e rumorosamente. “Fermati e prendi fuoco”, come si dice in gergo. Il modo che Python ha di fermarsi e prendere fuoco è sollevare un’eccezione.
</ol>
<p>La domanda da porsi è: “Come posso esprimere questa idea sotto forma di un requisito collaudabile?” Che ne dite di questo, per cominciare?
<blockquote>
<p>La funzione <code>to_roman()</code> dovrebbe sollevare un’eccezione di tipo <code>OutOfRangeError</code> quando le viene passato un intero più grande di <code>3999</code>.
</blockquote>
<p>Che aspetto potrebbe avere il test?
<p class=d>[<a href=esempi/romantest2.py>scarica <code>romantest2.py</code></a>]
<pre class=pp><code><a>class ToRomanBadInput(unittest.TestCase): <span class=u>①</span></a>
<a> def test_too_large(self): <span class=u>②</span></a>
'''to_roman dovrebbe fallire con numeri grandi'''
<a> self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) <span class=u>③</span></a></code></pre>
<ol>
<li>Come nel test precedente, create una classe che eredita da <code>unittest.TestCase</code>. Potete avere più di un test per classe (come vedrete più avanti in questo capitolo), ma in questo caso ho scelto di creare una nuova classe perché questo collaudo è qualcosa di diverso rispetto al precedente. Raggrupperemo tutti i collaudi sugli ingressi validi in una classe e tutti i collaudi sugli ingressi non validi in un’altra.
<li>Come nel test precedente, il test vero e proprio è un metodo della classe, con un nome che comincia con <code>test</code>.
<li>La classe <code>unittest.TestCase</code> fornisce il metodo <code>assertRaises()</code>, che prende i seguenti argomenti: l’eccezione che vi aspettate, la funzione che state collaudando e gli argomenti che volete passare a quella funzione. (Se la funzione che state collaudando prende più di un argomento, passateli tutti ad <code>assertRaises()</code>, in ordine, ed essi verranno passati direttamente alla funzione che state collaudando.)
</ol>
<p>Fate molta attenzione a quell’ultima riga di codice. Invece di invocare direttamente <code>to_roman()</code> e controllare manualmente che sollevi una particolare eccezione (incorporando la chiamata di funzione in <a href=il-vostro-primo-programma-python.html#exceptions>un blocco <code>try...except</code></a>), state utilizzando il metodo <code>assertRaises()</code>, che ha incapsulato per noi tutte queste operazioni. Tutto quello che dovete fare è dirgli quali sono l’eccezione che vi aspettate (<code>roman2.OutOfRangeError</code>), la funzione da invocare (<code>to_roman()</code>) e gli argomenti con cui invocarla (<code>4000</code>). Il metodo <code>assertRaises()</code> si prende cura di chiamare <code>to_roman()</code> e di controllare che sollevi <code>roman2.OutOfRangeError</code>.
<p>Notate anche che state passando la funzione <code>to_roman()</code> stessa come un argomento; non la state invocando e non ne state passando il nome sotto forma di stringa. Ho recentemente menzionato quant’è comodo che <a href=il-vostro-primo-programma-python.html#everythingisanobject>ogni cosa in Python sia un oggetto</a>?
<p>Quindi cosa succede quando lanciate la serie di test con questo nuovo test?
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest2.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_too_large (__main__.ToRomanBadInput)
<a>to_roman dovrebbe fallire con numeri grandi ... ERROR <span class=u>①</span></a>
======================================================================
ERROR: to_roman dovrebbe fallire con numeri grandi
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest2.py", line 78, in test_too_large
self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
<a>AttributeError: 'module' object has no attribute 'OutOfRangeError' <span class=u>②</span></a>
----------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (errors=1)</samp></pre>
<ol>
<li>Avreste dovuto aspettarvi che questo test fallisse (dato che non avete scritto ancora il codice per farlo passare) ma… non è esattamente “fallito”, bensì c’è stato un “errore”. Questa è una distinzione sottile ma importante. Un test di unità ha effettivamente <em>tre</em> valori di ritorno: successo, fallimento ed errore. Successo, ovviamente, significa che il test è passato — il codice ha fatto quello che vi aspettavate. “Fallimento” è il risultato del test precedente (fino a quando non avete scritto il codice per farlo passare) — il codice veniva eseguito ma il risultato non era quello che vi aspettavate. “Errore” significa che il codice non è nemmeno stato eseguito correttamente.
<li>Perché il codice non è stato eseguito correttamente? Ce lo dice la traccia dello stack di esecuzione: il modulo che state collaudando non include alcuna eccezione chiamata <code>OutOfRangeError</code>. Ricordate, avete passato questa eccezione al metodo <code>assertRaises()</code> perché è l’eccezione che volete che la funzione sollevi quando riceve un ingresso fuori dall’intervallo consentito. Ma l’eccezione non esiste, quindi la chiamata al metodo <code>assertRaises()</code> fallisce. Il metodo non ha avuto la possibilità di collaudare la funzione <code>to_roman()</code>; non è arrivato così lontano.
</ol>
<p>Per risolvere questo problema, dovete definire l’eccezione <code>OutOfRangeError</code> in <code>roman2.py</code>.
<pre class=pp><code><a>class OutOfRangeError(ValueError): <span class=u>①</span></a>
<a> pass <span class=u>②</span></a></code></pre>
<ol>
<li>Le eccezioni sono classi. Un errore di tipo “fuori dall’intervallo” è un tipo di errore sui valori — il valore passato come argomento è fuori dal suo intervallo accettabile. Così questa eccezione eredita dall’eccezione built-in <code>ValueError</code>. Questo non è strettamente necessario (avrebbe potuto semplicemente ereditare dalla classe base <code>Exception</code>) ma suona corretto.
<li>Le eccezioni non fanno effettivamente niente, ma avete bisogno di almeno una riga di codice per costruire una classe. L’istruzione <code>pass</code> non fa proprio nulla, ma è una riga di codice Python e quindi costituisce una classe.
</ol>
<p>Ora lanciate nuovamente la serie di test.
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest2.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_too_large (__main__.ToRomanBadInput)
<a>to_roman dovrebbe fallire con numeri grandi ... FAIL <span class=u>①</span></a>
======================================================================
FAIL: to_roman dovrebbe fallire con numeri grandi
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest2.py", line 78, in test_too_large
self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
<a>AssertionError: OutOfRangeError not raised by to_roman <span class=u>②</span></a>
----------------------------------------------------------------------
Ran 2 tests in 0.016s
FAILED (failures=1)</samp></pre>
<ol>
<li>Il nuovo test non è ancora passato, ma non restituisce nemmeno un errore. Invece, il test fallisce. Questo è un passo avanti! Significa che la chiamata al metodo <code>assertRaises()</code> questa volta ha avuto successo e che il framework per il collaudo di unità ha effettivamente collaudato la funzione <code>to_roman()</code>.
<li>Naturalmente, la funzione <code>to_roman()</code> non ha sollevato l’eccezione <code>OutOfRangeError</code> che avete appena definito, perché non gli avete ancora detto di farlo. Questa è una notizia eccellente! Significa che questo è un test valido — fallisce prima che scriviate il codice per farlo passare.
</ol>
<p>Ora potete scrivere il codice per far passare questo test.
<p class=d>[<a href=esempi/roman2.py>scarica <code>roman2.py</code></a>]
<pre class=pp><code>def to_roman(n):
'''converte un intero in un numero romano'''
if n > 3999:
<a> raise OutOfRangeError("numero fuori dall'intervallo (deve essere minore di 4000)") <span class=u>①</span></a>
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result</code></pre>
<ol>
<li>Questo è semplice: se l’ingresso dato (<var>n</var>) è più grande di <code>3999</code>, allora solleva un’eccezione di tipo <code>OutOfRangeError</code>. Il test di unità non controlla il messaggio nella stringa che accompagna l’eccezione, sebbene possiate scrivere un altro test che faccia questo controllo (ma fate attenzione ai problemi di internazionalizzazione per stringhe che variano con l’ambiente o la lingua dell’utente).
</ol>
<p>Questo fa passare il test? Scopriamolo.
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest2.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_too_large (__main__.ToRomanBadInput)
<a>to_roman dovrebbe fallire con numeri grandi ... ok <span class=u>①</span></a>
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK</samp></pre>
<ol>
<li>Urrà! Entrambi i test passano. Dato che avete lavorato iterativamente, spostandovi avanti e indietro tra i test e il codice, potete essere sicuri che le due righe di codice che avete scritto sono la causa del passaggio di quel test dal “fallimento” al “successo”. Questo tipo di confidenza si guadagna a un certo prezzo, ma si ripagherà da sola durante la vita del vostro codice.
</ol>
<p class=a>⁂
<h2 id=romantest3>Più fermate, più fuoco</h2>
<p>Insieme al collaudo per numeri troppo grandi, avete anche bisogno di collaudare numeri che sono troppo piccoli. Come <a href=#divingin>abbiamo notato nei nostri requisiti funzionali</a>, i numeri romani non possono esprimere lo 0 o i numeri negativi.
<pre class='nd screen'>
<samp class=p>>>> </samp><kbd class=pp>import roman2</kbd>
<samp class=p>>>> </samp><kbd class=pp>roman2.to_roman(0)</kbd>
<samp class=pp>''</samp>
<samp class=p>>>> </samp><kbd class=pp>roman2.to_roman(-1)</kbd>
<samp class=pp>''</samp></pre>
<p><em>Questo</em> non va bene. Aggiungiamo un test per ognuna di queste condizioni.
<p class=d>[<a href=esempi/romantest3.py>scarica <code>romantest3.py</code></a>]
<pre class=pp><code>class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman dovrebbe fallire con numeri grandi'''
<a> self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000) <span class=u>①</span></a>
def test_zero(self):
'''to_roman dovrebbe fallire con il numero 0'''
<a> self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) <span class=u>②</span></a>
def test_negative(self):
'''to_roman dovrebbe fallire con numeri negativi'''
<a> self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) <span class=u>③</span></a></code></pre>
<ol>
<li>Il metodo <code>test_too_large()</code> non è cambiato rispetto al passo precedente. Lo includo qui per mostrare dove trova posto il nuovo codice.
<li>Ecco un nuovo test: il metodo <code>test_zero()</code>. Come il metodo <code>test_too_large()</code>, dice al metodo <code>assertRaises()</code> definito in <code>unittest.TestCase</code> di chiamare la nostra funzione <code>to_roman()</code> con un parametro pari a 0 e di verificare che sollevi l’eccezione appropriata, <code>OutOfRangeError</code>.
<li>Il metodo <code>test_negative()</code> è quasi identico, senonché passa <code>-1</code> alla funzione <code>to_roman()</code>. Se uno di questi nuovi test <em>non</em> solleva un’eccezione di tipo <code>OutOfRangeError</code> (perché la funzione restituisce un valore, o perché solleva qualche altra eccezione) il collaudo si considera fallito.
</ol>
<p>Ora verifichiamo che i test falliscano:
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest3.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_negative (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri negativi ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri grandi ... ok
test_zero (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con il numero 0 ... FAIL
======================================================================
FAIL: to_roman dovrebbe fallire con numeri negativi
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest3.py", line 86, in test_negative
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
AssertionError: OutOfRangeError not raised by to_roman
======================================================================
FAIL: to_roman dovrebbe fallire con il numero 0
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest3.py", line 82, in test_zero
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
AssertionError: OutOfRangeError not raised by to_roman
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=2)</samp></pre>
<p>Eccellente. Entrambi i test falliscono, come era da aspettarsi. Ora passiamo al codice e vediamo cosa possiamo fare per farli passare.
<p class=d>[<a href=esempi/roman3.py>scarica <code>roman3.py</code></a>]
<pre class=pp><code>def to_roman(n):
'''converte un intero in un numero romano'''
<a> if not (0 < n < 4000): <span class=u>①</span></a>
<a> raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 3999)") <span class=u>②</span></a>
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result</code></pre>
<ol>
<li>Questa è una classica scorciatoia di Python: molteplici confronti allo stesso tempo. Questo codice è equivalente a <code>if not ((0 < n) and (n < 4000))</code>, ma è molto più facile da leggere. Questa riga di codice dovrebbe catturare gli ingressi che sono troppo grandi, negativi, o zero.
<li>Se cambiate le vostre condizioni, assicuratevi di aggiornare i vostri messaggi in modo che riflettano i cambiamenti. Il framework <code>unittest</code> non se ne curerà, ma sarebbe difficile effettuare un controllo manuale se il vostro codice lanciasse eccezioni non correttamente descritte.
</ol>
<p>Potrei fornirvi un’intera serie di esempi non correlati per mostrarvi che la scorciatoia dei molteplici confronti allo stesso tempo funziona, ma invece eseguirò semplicemente i test di unità e lo dimostrerò.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest3.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_negative (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri negativi ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri grandi ... ok
test_zero (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con il numero 0 ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.016s
OK</samp></pre>
<p class=a>⁂
<h2 id=romantest4>E ancora una cosa…</h2>
<p>C’era ancora un <a href=#divingin>requisito funzionale</a> per la conversione dei numeri in numeri romani: gestire i numeri non interi.
<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>import roman3</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>roman3.to_roman(0.5)</kbd> <span class=u>①</span></a>
<samp class=pp>''</samp>
<a><samp class=p>>>> </samp><kbd class=pp>roman3.to_roman(1.0)</kbd> <span class=u>②</span></a>
<samp class=pp>'I'</samp></pre>
<ol>
<li>Oh, questo non va bene.
<li>Oh, questo è ancora peggio. Entrambi i casi dovrebbero sollevare un’eccezione. Invece, danno risultati fasulli.
</ol>
<p>Fare il collaudo per i numeri non interi non è difficile. Prima di tutto, definite un’eccezione chiamata <code>NotIntegerError</code>.
<pre class='nd pp'><code># roman4.py
class OutOfRangeError(ValueError): pass
<mark>class NotIntegerError(ValueError): pass</mark></code></pre>
<p>Poi, scrivete un test che verifichi la generazione dell’eccezione <code>NotIntegerError</code>.
<pre class='nd pp'><code>class ToRomanBadInput(unittest.TestCase):
.
.
.
def test_non_integer(self):
'''to_roman dovrebbe fallire con numeri non interi'''
<mark> self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)</mark></code></pre>
<p>Ora contollate che il test fallisca in maniera appropriata.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest4.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_negative (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri negativi ... ok
test_non_integer (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri non interi ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri grandi ... ok
test_zero (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con il numero 0 ... ok
======================================================================
FAIL: to_roman dovrebbe fallire con numeri non interi
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest4.py", line 90, in test_non_integer
self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
<mark>AssertionError: NotIntegerError not raised by to_roman</mark>
----------------------------------------------------------------------
Ran 5 tests in 0.000s
FAILED (failures=1)</samp></pre>
<p>Scrivete il codice che fa passare il test.
<pre class=pp><code>def to_roman(n):
'''converte un intero in un numero romano'''
if not (0 < n < 4000):
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 3999)")
<a> if not isinstance(n, int): <span class=u>①</span></a>
<a> raise NotIntegerError('numeri non interi non possono essere convertiti') <span class=u>②</span></a>
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result</code></pre>
<ol>
<li>La funzione built-in <code>isinstance()</code> verifica che una variabile sia di un particolare tipo (o, tecnicamente, di un qualsiasi tipo discendente).
<li>Se l’argomento <var>n</var> non è un <code>int</code>, sollevate la nostra eccezione <code>NotIntegerError</code> appena coniata.
</ol>
<p>Infine, controllate che il codice faccia effettivamente passare il test.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest4.py -v</kbd>
<samp>test_to_roman_known_values (__main__.KnownValues)
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
test_negative (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri negativi ... ok
test_non_integer (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri non interi ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con numeri grandi ... ok
test_zero (__main__.ToRomanBadInput)
to_roman dovrebbe fallire con il numero 0 ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK</samp></pre>
<p>La funzione <code>to_roman()</code> passa tutti i propri test e non me ne vengono in mente altri, quindi è il momento di passare a <code>from_roman()</code>.
<p class=a>⁂
<h2 id=romantest5>Una piacevole simmetria</h2>
<p>Convertire una stringa da un numero romano in un intero sembra più difficile che convertire un intero in un numero romano. Di certo c’è il problema della validazione. È facile controllare se un intero è più grande di 0, ma è un po’ più difficile controllare se una stringa contiene un numero romano valido. Tuttavia abbiamo già costruito <a href=espressioni-regolari.html#romannumerals>un’espressione regolare per controllare i numeri romani</a>, quindi questa parte è fatta.
<p>Questo ci lascia il problema di convertire la stringa. Come vedremo fra un minuto, grazie alla ricca struttura dati che abbiamo definito per correlare i singoli numeri romani ai loro valori interi, il succo della funzione <code>from_roman()</code> è tanto semplice quanto quello della funzione <code>to_roman()</code>.
<p>Ma prima, i test. Avremo bisogno di un test sui “valori noti” per compiere una verifica a campione sull’accuratezza. La nostra serie di test contiene già <a href=#romantest1>una corrispondenza tra valori noti</a>, perciò riutilizziamola.
<pre class='nd pp'><code> def test_from_roman_known_values(self):
'''from_roman dovrebbe dare un risultato noto con un ingresso noto'''
for integer, numeral in self.known_values:
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)</code></pre>
<p>C’è una piacevole simmetria qui. Le funzioni <code>to_roman()</code> e <code>from_roman()</code> sono una l’inverso dell’altra. La prima converte interi in stringhe in un formato particolare, la seconda converte stringhe in un formato particolare in interi. In teoria, dovremmo essere in grado di far compiere un “viaggio di andata e ritorno” a un numero passandolo alla funzione <code>to_roman()</code> per ottenere una stringa, poi passando quella stringa alla funzione <code>from_roman()</code> per ottenere un intero, ritrovandoci infine con lo stesso numero.
<pre class='nd pp'><code>n = from_roman(to_roman(n)) per tutti i valori di n</code></pre>
<p>In questo caso, “tutti i valori” significa tutti i numeri compresi nell’intervallo <code>1..3999</code>, dato che questo è l’intervallo di ingressi validi per la funzione <code>to_roman()</code>. Possiamo esprimere questa simmetria in un test che scorre tutti i valori tra <code>1</code> e <code>3999</code>, invoca <code>to_roman()</code>, invoca <code>from_roman()</code> e controlla che il risultato sia uguale al numero originale.
<pre class='nd pp'><code>class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n per tutti gli n'''
for integer in range(1, 4000):
numeral = roman5.to_roman(integer)
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)</code></pre>
<p>Questi nuovi test non sono ancora nemmeno in grado di fallire. Non abbiamo definito alcuna funzione <code>from_roman()</code>, quindi solleveranno semplicemente qualche errore.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest5.py</kbd>
<samp>E.E....
======================================================================
ERROR: test_from_roman_known_values (__main__.KnownValues)
from_roman dovrebbe dare un risultato noto con un ingresso noto
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 78, in test_from_roman_known_values
result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'
======================================================================
ERROR: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n per tutti gli n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 103, in test_roundtrip
result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'
----------------------------------------------------------------------
Ran 7 tests in 0.019s
FAILED (errors=2)</samp></pre>
<p>Un agile scheletro di funzione risolverà questo problema.
<pre class='nd pp'><code># roman5.py
def from_roman(s):
'''converte un numero romano in un intero'''</code></pre>
<p>(Ehi, avete notato? Ho definito una funzione con solo una <a href=il-vostro-primo-programma-python.html#docstrings><code>docstring</code></a>. Questo è legale in Python. In effetti, alcuni programmatori venerano questa possibilità. “Non create scheletri vuoti, documentate!”)
<p>Ora i test effettivamente falliranno.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest5.py</kbd>
<samp>F.F....
======================================================================
FAIL: test_from_roman_known_values (__main__.KnownValues)
from_roman dovrebbe dare un risultato noto con un ingresso noto
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 79, in test_from_roman_known_values
self.assertEqual(integer, result)
AssertionError: 1 != None
======================================================================
FAIL: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n per tutti gli n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 104, in test_roundtrip
self.assertEqual(integer, result)
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 7 tests in 0.002s
FAILED (failures=2)</samp></pre>
<p>Ora è tempo di scrivere la funzione <code>from_roman()</code>.
<pre class=pp><code>def from_roman(s):
'''converte un numero romano in un intero'''
result = 0
index = 0
for numeral, integer in roman_numeral_map:
<a> while s[index:index+len(numeral)] == numeral: <span class=u>①</span></a>
result += integer
index += len(numeral)
return result</code></pre>
<ol>
<li>Lo schema qui è lo stesso della funzione <a href=#romantest1><code>to_roman()</code></a>. Iterate attraverso la struttura dati dei numeri romani (una tupla di tuple), ma invece di trovare una corrispondenza con i valori interi più alti quanto più spesso è possibile, trovate una corrispondenza con le stringhe dei caratteri numerici romani “più alti” quanto più spesso è possibile.
</ol>
<p>Se non vi è chiaro come funziona <code>from_roman()</code>, aggiungete una chiamata a <code>print()</code> alla fine del ciclo <code>while</code>:
<pre><code>def from_roman(s):
'''converte un numero romano in un intero'''
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
<mark> print('trovato', numeral, 'di lunghezza', len(numeral), ', aggiungo', integer)</mark></code></pre>
<pre class='nd screen'>
<samp class=p>>>> </samp><kbd class=pp>import roman5</kbd>
<samp class=p>>>> </samp><kbd class=pp>roman5.from_roman('MCMLXXII')</kbd>
<samp class=pp>trovato M di lunghezza 1, aggiungo 1000
trovato CM di lunghezza 2, aggiungo 900
trovato L di lunghezza 1, aggiungo 50
trovato X di lunghezza 1, aggiungo 10
trovato X di lunghezza 1, aggiungo 10
trovato I di lunghezza 1, aggiungo 1
trovato I di lunghezza1, aggiungo 1
1972</samp></pre>
<p>Tempo di rieseguire i test.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest5.py</kbd>
<samp>.......
----------------------------------------------------------------------
Ran 7 tests in 0.060s
OK</samp></pre>
<p>Due notizie eccitanti qui. La prima è che la funzione <code>from_roman()</code> converte con successo gli ingressi validi, almeno per tutti i <a href=#romantest1>valori noti</a>. La seconda è che anche il test di “andata e ritorno” è passato. Combinato con i test sui valori noti, può farvi sentire ragionevolmente sicuri che entrambe le funzioni <code>to_roman()</code> e <code>from_roman()</code> convertano correttamente tutti i possibili valori validi. (Questo non è garantito; è teoricamente possibile che <code>to_roman()</code> abbia un bug che le fa produrre il numero romano sbagliato per un particolare insieme di ingressi <em>e</em> che <code>from_roman()</code> abbia un bug reciproco che produce gli stessi valori interi errati proprio per quell’insieme di numeri romani che <code>to_roman()</code> genera in maniera scorretta. A seconda della vostra applicazione e dei vostri requisiti, questa possibilità potrebbe infastidirvi; se è così, scrivete test più completi fino a quando non vi sentite tranquilli.)
<p class=a>⁂
<h2 id=romantest6>Altri ingressi non validi</h2>
<p>Ora che la funzione <code>from_roman()</code> funziona correttamente con ingressi validi, è il momento di inserire l’ultimo pezzo del puzzle: farla funzionare correttamente con ingressi non validi. Questo significa trovare un modo per esaminare una stringa e determinare se è un numero romano valido. Questa operazione è intrinsecamente più difficile rispetto alla <a href=#romantest3>validazione di ingressi numerici</a> compiuta nella funzione <code>to_roman()</code>, ma avete uno strumento potente a vostra disposizione: le espressioni regolari. (Se non avete familiarità con le espressioni regolari, questo sarebbe un buon momento per leggere <a href=espressioni-regolari.html>il capitolo sulle espressioni regolari</a>.)
<p>Come avete visto nel <a href=espressioni-regolari.html#romannumerals>Caso di studio: numeri romani</a>, esistono alcune semplici regole per costruire un numero romano usando le lettere <code>M</code>, <code>D</code>, <code>C</code>, <code>L</code>, <code>X</code>, <code>V</code> e <code>I</code>. Rivediamo le regole.
<ul>
<li>Talvolta i caratteri sono additivi. <code>I</code> è <code>1</code>, <code>II</code> è <code>2</code>, e <code>III</code> è <code>3</code>. <code>VI</code> è <code>6</code> (letteralmente, “<code>5</code> e <code>1</code>”), <code>VII</code> è <code>7</code>, e <code>VIII</code> è <code>8</code>.
<li>I caratteri delle potenze di dieci (<code>I</code>, <code>X</code>, <code>C</code> e <code>M</code>) possono essere ripetuti fino a tre volte. A <code>4</code>, dovete sottrarre dal carattere del quintuplo più alto successivo. Non potete rappresentare <code>4</code> come <code>IIII</code>; invece, va rappresentato come <code>IV</code> (“<code>1</code> meno di <code>5</code>”). Il numero <code>40</code> è scritto come <code>XL</code> (“<code>10</code> meno di <code>50</code>”), <code>41</code> come <code>XLI</code>, <code>42</code> come <code>XLII</code>, <code>43</code> come <code>XLIII</code>, e poi <code>44</code> come <code>XLIV</code> (“<code>10</code> meno di <code>50</code>, poi <code>1</code> meno di <code>5</code>”).
<li>Talvolta i caratteri sono… l’opposto di additivi. Mettendo certi caratteri prima di altri, sottraete dal valore finale. Per esempio, a <code>9</code>, dovete sottrarre dal carattere della potenza di dieci più alta successiva: <code>8</code> è <code>VIII</code>, ma <code>9</code> è <code>IX</code> (“<code>1</code> meno di <code>10</code>”), non <code>VIIII</code> (dato che il carattere <code>I</code> non può essere ripetuto quattro volte). Il numero <code>90</code> è <code>XC</code>, <code>900</code> è <code>CM</code>.
<li>I caratteri dei quintupli non possono essere ripetuti. Il numero <code>10</code> è sempre rappresentato come <code>X</code>, mai come <code>VV</code>. Il numero <code>100</code> è sempre <code>C</code>, mai <code>LL</code>.
<li>I numeri romani vengono sempre letti da sinistra a destra, così l’ordine dei caratteri ha molta importanza. <code>DC</code> è <code>600</code>; <code>CD</code> è un numero completamente differente (<code>400</code>, “<code>100</code> meno di <code>500</code>”). <code>CI</code> è <code>101</code>; <code>IC</code> non è nemmeno un numero romano valido (perché non potete sottrarre <code>1</code> direttamente da <code>100</code>; dovreste scriverlo come <code>XCIX</code>, cioè “<code>10</code> meno di <code>100</code>, poi <code>1</code> meno di <code>10</code>”).
</ul>
<p>Quindi, un test utile sarebbe quello di assicurarsi che la funzione <code>from_roman()</code> fallisca quando le passate una stringa con cifre ripetute troppe volte. Quante siano “troppe” dipende dalla cifra.
<pre class='nd pp'><code>class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''from_roman dovrebbe fallire con cifre ripetute troppe volte'''
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)</code></pre>
<p>Un altro test utile sarebbe quello di controllare che certi pattern non vengano ripetuti. Per esempio, <code>IX</code> è <code>9</code>, ma <code>IXIX</code> non è mai valido.
<pre class='nd pp'><code> def test_repeated_pairs(self):
'''from_roman dovrebbe fallire con coppie di cifre ripetute'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)</code></pre>
<p>Un terzo test potrebbe controllare che le cifre appaiano nell’ordine corretto, dal valore più alto a quello più basso. Per esempio, <code>CL</code> è <code>150</code>, ma <code>LC</code> non è mai valido perché la cifra per <code>50</code> non può mai trovarsi prima della cifra per <code>100</code>. Questo test include un insieme di antecedenti non validi scelti a caso: <code>I</code> prima di <code>M</code>, <code>V</code> prima di <code>X</code>, e così via.
<pre class='nd pp'><code> def test_malformed_antecedents(self):
'''from_roman dovrebbe fallire con antecedenti malformati'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)</code></pre>
<p>Ognuno di questi test fa affidamento sul fatto che la funzione <code>from_roman()</code> sollevi una nuova eccezione, <code>InvalidRomanNumeralError</code>, che non abbiamo ancora definito.
<pre class='nd pp'><code># roman6.py
class InvalidRomanNumeralError(ValueError): pass</code></pre>
<p>Tutti e tre questi test dovrebbero fallire dato che attualmente la funzione <code>from_roman()</code> non effettua alcun controllo di validità. (Se non falliscono adesso, allora cosa diavolo stanno collaudando?)
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest6.py</kbd>
<samp>FFF.......
======================================================================
FAIL: test_malformed_antecedents (__main__.FromRomanBadInput)
from_roman dovrebbe fallire con antecedenti malformati
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 113, in test_malformed_antecedents
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
======================================================================
FAIL: test_repeated_pairs (__main__.FromRomanBadInput)
from_roman dovrebbe fallire con coppie di cifre ripetute
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 107, in test_repeated_pairs
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
======================================================================
FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput)
from_roman dovrebbe fallire con cifre ripetute troppe volte
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 102, in test_too_many_repeated_numerals
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
----------------------------------------------------------------------
Ran 10 tests in 0.058s
FAILED (failures=3)</samp></pre>
<p>Molto bene. Ora tutto quello che dobbiamo fare è aggiungere <a href=espressioni-regolari.html#romannumerals>l’espressione regolare per verificare i numeri romani validi</a> alla funzione <code>from_roman()</code>.
<pre class='nd pp'><code>roman_numeral_pattern = re.compile('''
^ # inizio della stringa
M{0,3} # migliaia - da 0 a 3 M
(CM|CD|D?C{0,3}) # centinaia - 900 (CM), 400 (CD), 0-300 (da 0 a 3 C),
# o 500-800 (D, seguita da 0 fino a 3 C)
(XC|XL|L?X{0,3}) # decine - 90 (XC), 40 (XL), 0-30 (da 0 a 3 X),
# o 50-80 (L, seguita da 0 fino a 3 X)
(IX|IV|V?I{0,3}) # unità - 9 (IX), 4 (IV), 0-3 (da 0 a 3 I),
# o 5-8 (V, seguita da 0 fino a 3 I)
$ # fine della stringa
''', re.VERBOSE)
def from_roman(s):
'''converte un numero romano in un intero'''
<mark> if not roman_numeral_pattern.search(s):
raise InvalidRomanNumeralError('Numero romano non valido: {0}'.format(s))</mark>
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index : index + len(numeral)] == numeral:
result += integer
index += len(numeral)
return result</code></pre>
<p>E rieseguire i test…
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest7.py</kbd>
<samp>..........
----------------------------------------------------------------------
Ran 10 tests in 0.066s
OK</samp></pre>
<p>E il premio anticlimax dell’anno va… alla parola “<code>OK</code>”, che viene stampata dal modulo <code>unittest</code> quando tutti i test passano.
<p class=v><a href=uso-avanzato-degli-iteratori.html rel=prev title='indietro a “Uso avanzato degli iteratori”'><span class=u>☜</span></a> <a href=refactoring.html rel=next title='avanti a “Refactoring”'><span class=u>☞</span></a>
<p class=c>© 2001–10 <a href=informazioni-sul-libro.html>Mark Pilgrim</a><br>
© 2009–10 <a href=informazioni-sulla-traduzione.html>Giulio Piancastelli</a> per la traduzione italiana
<script src=j/jquery.js></script>
<script src=j/prettify.js></script>
<script src=j/dip3.js></script>