ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 60
Abonament PDF

Teste unitare cu Spock

Bianca Leuca
Software Developer @ Mozaic Labs



TESTARE

Când am început primul nostru proiect, am căutat un tool ușor de folosit care ne-ar fi putut ajuta să scriem teste lizibile și concise. Ne-am hotărât să folosim Spock, pentru că în combinație cu Groovy ne dă exact ce avem nevoie: teste lizibile de tip BDD pe care le putem scrie și schimba rapid. Spock poate fi folosit și în combinație cu Java, dar are sinergie foarte bună cu Groovy.

Toate exemplele din articol sunt scrise în Groovy, dar e suficientă puțină experiență cu Java pentru a le înțelege. Deoarece exemplele de teste din proiectul nostru sunt prea complicate pentru o introducere în Spock, am ales exemple de cod dintr-un kata pe care l-am făcut pentru a exersa TDD.

Setup-ul inițial

E ușor să adaugi Spock într-un proiect Gradle. Acesta este fișierul meu build.gradle:

apply plugin: 'groovy'
repositories {
  mavenCentral()
}
dependencies {
  testCompile(
    'junit:junit:4.12',
    'org.codehaus.groovy:'+
       'groovy-all:2.4.4',
    'org.spockframework:
       'spock-core:1.0-groovy-2.4',
    )
    testRuntime(
    // for spock reports
    'com.athaydes:'
    +'spock-reports:1.2.7'
    )
}

Se scrie primul test care pică:

import spock.lang.Specification
class SeeIfItWorksSpec extends 
 Specification {
   def "it should fail"() {
      expect:
      false
   }
}

Outputul este:

Condition not satisfied:

false

Apoi, se înlocuiește false cu true. Dacă outputul este Tests PASSED, setupul este complet.

Sintaxa Spock

Denumirea testelor

Numele testelor în Spock sunt sub formă de Stringuri. Acest lucru permite scrierea de fraze întregi în nume care ajută la lizibilitatea testelor și permite înțelegerea lor doar din citirea numelui.

def "transforms 1 into roman
  numeral I and writes the result   
  into a file"() {

}

Blocuri

Prin extinderea clasei de bază, Specification, se pot accesa în teste șase tipuri de blocuri

(given, when, then, expect, where, cleanup). Cu ajutorul lor, se face o separare clară între precondiții, acțiunea care se testează și răspuns.

Exemplu:

Am ales ca exemplu transformarea unui număr arab într-un număr roman.

class NumeralTransformerSpec extends Specification {
  def numeralTransformer

  def setup() {
    numeralTransformer = 
      new NumeralTransformer()
   }

   def "transforms arabic numeral 1 into roman
      numeral I"() {
      given:
      def arabicNumeral = 1

      when:
      def result = numeralTransformer
         .transform(arabicNumeral)

      then:
      result == "I"
   }
}

Exemplul de mai sus se citește după cum urmează: dat fiind un număr arab, când este apelată metoda "transform" cu numărul respectiv ca parametru, atunci rezultatul așteptat este "I".

În metoda de setup se pune codul care rulează înainte de fiecare metodă de test. În exemplul de mai sus, obiectul testat este inițializat acolo pentru a evita duplicarea în alte metode de test.

Keywordul def din Groovy folosește la declararea unei variabile sau metode fără a-i specifica tipul. Este similar cu a o declara ca Object.

Blocul given

În blockul given se scrie setupul ințial pentru feature-ul testat. Codul poate conține inițializarea obiectului testat, mockuri și stuburi, declarări de variabile, etc. .

Blocurile when și then

Aceste blocuri sunt mereu împreună. În ele se specifică ce anume se testează și se verifică răspunsul. În timp ce blocul given este opțional, aceste două blocuri sunt prezente tot timpul, în afară de cazul în care este folosit blocul expect.

Codul din blocul then poate verifica trei lucruri: starea rezultatului, dacă a fost aruncată vreo excepție sau dacă au fost interacțiuni cu alte clase sau metode.

Verificarea stării

Codul care verifică starea unui obiect în Spock este similar cu asserturile din Junit, doar că aici se pot folosi expresii simple de tip boolean.

Să presupunem că NumeralTransformer din exemplul anterior are o metodă care mapează numărul arab cu reprezentarea lui romană și returnează mapul respectiv.

def "builds the numeral mapping for 1"() {
   when:
   def result = numeralTransformer.buildNumeralMap(1)
   then:
   result == [1: "I"]
}
Ce se întâmplă dacă schimbăm expresia verificată?

def "builds the numeral mapping for 1""() {
   when:
   def result = numeralTransformer.buildNumeralMap(1)
   then:
   result == [1: "II"]
}

Outputul devine:

Failure:  builds the numeral mapping(com.coolconf.unitTestsWithSpock.RomanTransformerSpec)
Condition not satisfied:
result  ==  [1: "II"]
   |     |
  [1:I] false

Verificarea excepțiilor

Se poate verifica dacă a fost aruncată o excepție în blocul then prin apelarea metodei thrown cu parametrul tipul presupusei excepții.

În exemplul următor, se verifică aruncarea unei excepții de tip IllegalArgumentException când numărul dat ca parametru este -1.

def "throws IllegalArgumentException if
   number is -1"() {
   when:
   numeralTransformer.transform(-1)

   then:
   thrown(IllegalArgumentException)
}

Verificarea interacțiunilor

Verificarea interacțiunilor înseamnă verificarea felului în care obiectul testat interacționează cu alte obiecte. Acest lucru este realizat folosind obiecte de tip Mock, asigurate de clasa MockingApi din Spock.

În această secțiune voi da doar un exemplu, voi detalia comportamentul obiectelor de tip Mock în secțiunea Mock-uri și Stub-uri.

class NumeralTransformerSpec extends Specification {
   def numeralTransformer

   def setup() {
      numeralTransformer = new NumeralTransformer()
      numeralTransformer.fileWriterService = 
         Mock(FileWriterService)
   }

   def "transforms 1 into roman numeral I and writes
      the result into a file"() {
      given:
      def arabicNumeral = 1
      def file = new File("/somepath")

      when:
      numeralTransformer
        .writeTheRomanTransformationToFile(file, 
         arabicNumeral)

      then:
      1 * numeralTransformer
        .fileWriterService.writesNumberToFile(file, 
         "I")
   }
}

În exemplul de mai sus, obiectul FileWriterService de tip Mock este creat în secțiunea de setup și interacțiunile sunt verificate în blocul then.

Exemplul se citește în felul următor: dat fiind un număr arab cu valoarea "1" și un fișier, când metoda

writeTheRomanTransformationToFile este apelată cu fișierul si numărul ca parametri, atunci metoda

writesNumberToFile() este apelată o singură dată cu fișierul și Stringul "I" ca parametri.

Blocul expect

Blocul expect se poate folosi când are mai mult sens plasarea în aceeași expresie a acțiunii și a rezultatului ei. Acest bloc folosește doar la verificarea unei stări, și mai poate conține doar declarări de variabile.

Unul din exemplele de mai sus se poate rescrie:

def "transforms 1 into roman value I"() {
   expect:
   numeralTransformer.transform(1) == "I"
}

Blocul Where

Acest bloc este util când testul este data driven, adică se dorește testarea aceleiași metode cu inputuri diferite. Cu ajutorul blocului where, se evită duplicarea.

def "transforms arabic numeral into roman numeral"() {
   when:
   def result = numeralTransformer.
     transform(arabicNumeral)

   then:
   result == expectedResult

   where:
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

În acest exemplu, blocul where conține un tabel de date. Antetul tabelului reprezintă variabilele și restul rândurilor sunt valorile lor. Fiecare rând reprezintă un caz de test și pentru fiecare din rânduri testul se va executa o dată, cu metoda de setup la începutul fiecărei interații și metoda de cleanup la sfârșit ( dacă metodele sunt prezente). Outputul unui test poate fi separat prin două pipeline-uri.

Adnotarea Unroll

@Unroll
def "transform arabic numeral #arabicNumeral into
   roman numeral #expectedResult"() {
   when:
   def result = numeralTransformer
     .transform(arabicNumeral)

   then:
   result == expectedResult
   where:
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

Scopul adnotării Unroll este de a face ca iterațiile să fie raportate independent. Acest lucru este util în special când unele din ele pică și ai nevoie să știi care din ele. În combinație cu placeholderele (variabilele din numele testului care încep cu "#"), se generează un raport lizibil:

--Output from transform arabic numeral 1 into roman value I--| Running 1 unit test... 2 of 2

--Output from transform arabic numeral 2 into roman value II--| Running 1 unit test... 3 of 3

--Output from transform arabic numeral 3 into roman value III--| Completed 3 unit tests, 0 failed in 0m 0s

Pipe-uri de date

Tabelele de date sunt doar syntactic sugar pentru pipe-urile de date. Exemplul de mai sus poate fi rescris în felul următor:

@Unroll
def "transform arabic numeral #arabicNumeral into
   roman numeral #expectedResult"() {
   when:
   def result = numeralTransformer
     .transform(arabicNumeral)

   then:
   result == expectedResult

   where:
   arabicNumeral << [1, 2, 3]
   expectedResult << ["I", "II", "III"]
}

Mockuri și Stuburi

Mockingul în Spock este permisiv, adică orice apel de metodă al obiectului mockuit care nu este relevant testului și nu este verificată interacțiunea cu el este permis și este dat un răspuns default pentru el (null, false sau zero).

Obiectele de tip Mock nu au comportament și nu trebuie confundate cu stuburile.

Interacțiuni

Să revedem examplul cu fileWriterService:

then:
  1 * numeralTransformer.fileWriterService
    .writesNumberToFile(file, "I")

În verificarea acțiunii sunt specificate următoarele lucruri:

cardinalitatea ( 1 *) : de câte ori ne așteptăm ca obiectul testat să interacționeze cu metodele colaboratorilor săi;

Unde se declarară interacțiunile?

Cel mai des declar interacțiunile în blocul then, dar nu este obligatoriu. Interacțiunile pot fi declarate înainte de blocul when, adică pot fi plasate în metoda de setup sau blocul given. De asemenea, pot fi declarate în momentul în care se creează mockurile sau după.

Interacțiuni care nu sunt respectate

Ce se întâmplă când o interacțiune nu este respectată? Se poate întâmpla din două motive principale: fie numărul interacțiunilor este specificat greșit, sau parametrii metodei sunt greșiți. Voi refolosi exemplul de mai sus dar voi schimba cardinalitatea interacțiunii.

def "transforms 1 into roman numeral I and writes the result into a file"() {
      given:
      def arabicNumeral = 1
      def file = new File("/somepath")

      when:
      numeralTransformer
        .writeTheRomanTransformationToFile(file, 
         arabicNumeral)

      then:
      0 * numeralTransformer
        .fileWriterService.writesNumberToFile(file, 
         "I")
}

Eroarea afișată este:

Too many invocations for:

0 * numeralTransformer.fileWriterService.writesNumberToFile(file, "I")   (1 invocation)
Matching invocations (ordered by last occurrence):

1 * <FileWriterService>.writesNumberToFile(/somepath, 'I')   <-- this triggered the error
Eroarea este destul de intuitivă: există o invocare a metodei writeTheRomanTransformationToFile în codul de producție și testul așteaptă zero invocări.
La polul opus, dacă se verifcă:

2 * numeralTransformer.fileWriterService
  .writesNumberToFile(file, "I")
se va afișa următoarea eroare:

Too few invocations for:

2 * numeralTransformer.fileWriterService
  .writesNumberToFile(file, "I")   (1 invocation)

Aceasta înseamnă că testul aștepta două invocări ale service callului, dar în codul de producție există doar una.

Stubbing

Stubbingul definește comportamentul unui colaborator (returnarea unei valori sau a unei secvențe de valori, aruncarea unei excepții).

Exemplu:

class RomanNumeralsArithmeticsSpec extends 
 Specification {
   def romanNumeralsArithmeticsService

   def setup() {
      romanNumeralsArithmeticsService = 
        new RomanNumeralsArithmeticsService()

      romanNumeralsArithmeticsService
       .numeralTransformerService =                 Mock(NumeralTransformerService)
   }

   def "check I + II = III"() {
     given:
     def first = "I"
     def second = "II"

     and:
     romanNumeralsArithmeticsService
      .numeralTransformerService.fromRoman(first) >> 1

    romanNumeralsArithmeticsService
    .numeralTransformerService.fromRoman(second) >> 2

    romanNumeralsArithmeticsService
    .numeralTransformerService.transform(3) >> "III"

    when:
    def sum = romanNumeralsArithmeticsService
    .sum(first, second)

     then:
     sum == "III"
   }
}

În acest exemplu este verificată suma a două numere romane. Numeral Tranformer Service este stubbed și metodele lui sunt "forțate" să întoarcă anumite valori care vor fi folosite în codul de producție.

Exemplul se citește în felul următor: fiind date două numere romane, și metodele colaboratorului RomanNumeralsArithmeticsService întorc valorile 1, 2 și "III", atunci suma lor este "III".

Gruparea interacțiunilor și a stubbingului

Stubbingul și verificarea interacțiunilor se pot face în același timp. De asemenea, metodele aceluiași obiect de tip Mock se pot grupa:

  def "check I + II = III"() {
      given:
      def first = "I"
      def second = "II"
      romanNumeralsArithmeticsService
       .numeralTransformerService =                 Mock(NumeralTransformerService) {
         1 * fromRoman(first) >> 1
         1 * fromRoman(second) >> 2
         1 * transform(3) >> "III"
      }

      when:
      def sum = romanNumeralsArithmeticsService
       .sum(first, second)

      then:
      sum == "III"
   }
}

Spock Reports

Spock permite descrieri de tip text în blocuri:

@Unroll
def "transforms arabic numeral #arabicNumeral
   into roman numeral #expectedResult"() {
   when: "Calls transform method on arabic numeral"
   def result = numeralTransformer
     .transform(arabicNumeral)

   then: "Expected result is '#expectedResult'"
   result == expectedResult

   where: "Arabic numeral is #arabicNumeral"
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

Datorită pluginului Spock-Reports-Plugin, pot fi generate rapoarte lizibile care permit utilizarea testelor ca documentație:

Report for NumeralTransformerSpec

Summary:

Created on Fri May 05 22:10:34 EEST 2017 by biancal

+-------------------+----------+--------+--------------+-----------+
| Executed features | Failures | Errors | Success rate | Time      |
+-------------------+----------+--------+--------------+-----------+
| 3                 | 0        | 0      | 100.0%       | 0.074 sec |
+-------------------+----------+--------+--------------+-----------+

Features:

transforms arabic numeral 1 into roman numeral I

transforms arabic numeral 2 into roman numeral II

transforms arabic numeral 3 into roman numeral III

+------------------------------------------+------------------------------------------+
| transforms arabic numeral 1 into roman   |                                          |
| numeral I                                |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'I'                   |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 1                      |
+------------------------------------------+------------------------------------------+
| transforms arabic numeral 2 into roman   |                                          |
| numeral II                               |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'II'                  |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 2                      |
+------------------------------------------+------------------------------------------+
| transforms arabic numeral 3 into roman   |                                          |
| numeral III                              |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'III'                 |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 3                      |
+------------------------------------------+------------------------------------------+

Rapoartele generate sunt în special utile pentru feature-uri higher-level, care pot fi citite și de către alți membri ai echipei care nu sunt programatori (de exemplu, de către product owneri)

Concluzii

Spock este recomandat programatorilor care lucrează cu Groovy sau Java și care au nevoie de un framework de testare care are o curbă de învățare rapidă, generează rapoarte lizibile de tip BDD și are o librărie de mocking permisivă care nu aglomerează codul cu verificări de interacțiuni irelevante.

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects