Groovy Spock – testy jednostkowe

Kontynuując pracę nad rest api z poprzedniego wpisu kolejnym krokiem jest dodanie testów jednostkowych. Póki co mamy bardzo prosty serwis, który możemy przetestować używając wygodnego Spock Test Api. Oczywiście można by to zrobić za pomocą standardowego JUnit i kilku bibliotek pomocniczych, ale chciałbym Ci pokazać jak prostym i wygodnym narzędziem jest Spock, który sam w sobie zastępuje JUnit, Mockito, jMock itd. Zaletą Spock jest również to, że może on być użyty niezależnie od tego czy Twój projekt jest w języku groovy czy java.

 



Testowany będzie komponent CarService:

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
@Service
@CompileStatic
class CarService {

    @Autowired
    private CarRepository carRepository

    List<Car> fetchCarsList() {
        carRepository.findAll()
    }

    Car getCar(long id) {
        carRepository.findById(id)
    }

    Car saveCar(Car car, Long id=null) {
        if (id) {
            car.carId = id
        }
        carRepository.save(car)
    }

    void deleteCar(long id) {
        Car car = carRepository.findById(id)
        if (car)
            carRepository.delete(car)
    }
}

 

Dodanie Zależności

Na początek musimy dodać bibliotekę Spock dla testCompile w build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
buildScript {
  ext: {
    ...
    spockVersion = '1.1-groovy-2.4-rc-2'
    cglibVersion = '3.2.6'
  }
}
dependencies {
  ...
  testCompile "cglib:cglib-nodep:${cglibVersion}"
  testCompile "org.spockframework:spock-core:${spockVersion}"
}

Dobrą praktyką jest wyciąganie wersji bibliotek do zmiennych w buildScript.ext jak wyżej. Ułatwi to znacząco zarządzanie i aktualizację bibliotek w przyszłości. Dodałem również zależność do cglib-nodep aby móc tworzyć obiekty mock klas bez interfejsów.
 

Test jednostkowy Spock

Na razie mamy tylko jeden prosty serwis, który wystawia podstawowe operacje na modelu, ale wystarczy aby przedstawić podstawowe możliwości Spock.
Do pisania testów specyfikacji Spock możemy skorzystać w 100% z dobrodziejstw języka groovy, aby kod był prosty i czytelny.
 
Groovy spock podąża za konwencją specification as documentation – chodzi o to aby test był czytelny i przejrzysty jak dokumentacja. Czytając kod testu można zrozumieć jak działa program. Podstawowa definicja nowego testu pojedynczej funkcjonalności wygląda następująco:

1
2
3
4
5
6
7
8
def 'should fetch list of cars'() {
    given:
    // przygotowujemy dane testowe
    when:
    // wywołujemy testowaną funkcję
    then:
    // sprawdzamy poprawność otrzymanych rezultatów    
}

W powyższym przykładzie już sama nazwa funkcji def 'should fetch list of cars'() jasno deklaruje jak działać powinna testowana funkcjonalność. given, when i then są słowami kluczowymi, które oddzielają podstawowe bloki testu.

A implementacja będzie wyglądać tak:

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
class CarServiceTest extends Specification {

    CarRepository repository = Mock(CarRepository) // mock klasy CarRepository
    CarService carService = new CarService(carRepository: repository) // testowany serwis z przekazanym mock repository

    def 'should fetch list of cars'() {
        given:
        // w tym wypadku nie potrzebujemy danych testowych więc można by usunąć blok 'given'

        when:
        List<Car> cars = carService.fetchCarsList()

        then:
        // sprawdzamy, że funkcja findAll() wykonała się dokładnie 1 raz:
        1 * repository.findAll() >> [
                new Car(1L, BMW, "M4", 2017, 34000L, LocalDate.now()),
                new Car(2L, AUDI, "A3", 2016, 46000L, LocalDate.now())
        ]
        // ponieważ repository jest mockiem, możemy za pomocą '>>' zadeklarować co ma zwrócić funkcja findAll().
        // jeśli byśmy tego nie zrobili funkcja zwróciłaby null. Tu zwracamy listę dwóch obiektów Car.

        // poniżej sprawdzamy wartości zwrócone przez fetchCarsList().
        // Zauważ, że nie potrzebujemy żadnych funkcji assert, samo '==' w zupełności wystarczy.
        cars.size() == 2      
        cars[0].brand == BMW
        cars[1].brand == AUDI
        ...
    }
}

Powyżej przetestowaliśmy zwracanie listy pojazdów. Przykład pokazał nam jak możemy mockować komponenty oraz sprawdzać poprawność danych.

 

Zestaw danych testowych

Przetestujmy teraz trochę bardziej skomplikowaną funkcję tworzenia nowego elementu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Unroll    
def 'should save car'() {      
    given:
    // testowy obiekt Car z parametrem 'brand' z where
    Car newCar = new Car(null, testBrand, "C AMG", 2014, 98000, LocalDate.now())

    when:
    Car created = carService.saveCar(newCar, carId)

    then:
    1 * repository.save(newCar) >> newCar
    // wykorzystanie parametrów z where
    created.carId == carId
    created.brand == testBrand
    ...

    where:
    carId | testBrand
    1l    | MERCEDES
    null  | BMW
}

W ramach przykładu sparametryzowałem w bloku where dwie zmienne carId oraz testBrand, których używam w teście. W ten sposób test w momencie uruchomienia jest niejako odpalany dwa razy – raz z catId=1l i testBrand=MERCEDES oraz drugi raz z wartościami odpowiednio null i BMW. Widać to dobrze dzięki opcjonalnej adnotacji @Unroll:

 

Podsumowanie

Pozostałe funkcje getCar() oraz deleteCar() muszą być przetestowane w analogiczny sposób. Pozostawiam to Tobie jako zadanie do przećwiczenia samemu. 🙂 Zostaw komentarz lub napisz do mnie jeśli masz pytania.

Posted on: Kwiecień 8, 2018

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *