Flutter. Bez Equatable nawet nie zaczynaj testować.
Pokażę Ci bibliotekę Equatable, która pozwoli Ci na uproszczenie procesu porównywania obiektów w testach jednostkowych.
Język Dart domyślnie stosuje podczas porównywania obiektów sprawdzenie, czy ich referencje są takie same. Oznacza to, że dwa obiekty tej samej klasy pomimo tożsamych wartości pól będą różne przy wykorzystaniu domyślnej metody porównania. Jest to problematyczne podczas pisania testów jednostkowych, kiedy to najczęstszym sposobem (o ile nie jedynym) jest porównywanie obiektu pod względem wartości z innym. W dzisiejszym wpisie przedstawię Ci bibliotekę Equatable, która w przejrzysty sposób pomaga rozwiązać ten problem.
Jakiś czas temu opisałem na blogu moją przygodę z rozpoczęciem korzystania z podejścia TDD.
Przekonaj się
Na potwierdzenie moich słów poniżej przedstawiam Ci kilka linijek kodu, w których sprawdzam równość obiektów, w trzech przypadkach.
void main() {
//First
Cat first = Cat("Filemon");
Cat second = Cat("Tiger");
print("First: ${first == second}");
//Second
first = Cat("Tiger");
second = Cat("Tiger");
print("Second: ${first == second}");
//Third
first = Cat("Tiger");
second = first;
print("Third: ${first == second}");
}
class Cat {
String _name;
Cat(this._name);
}
Poniżej przedstawiłem Ci wydruk z konsoli. Biorąc pod uwagę sposób porównywania obiektów w Dart, nie ma zaskoczenia.
First: false
Second: false
Third: true
Testy
No dobrze. Wiemy już, jak język Dart porównuje obiekty. Zanim przejdziemy do rozwiązania, przedstawię Ci kod, który poddamy testom jednostkowym. Nie będzie to nic spektakularnego, ale wystarczy do przedstawienia koncepcji. Do dzieła!
class Gym {
Human increaseStrength(Human human, int value) {
human.increaseStrength(value);
return human;
}
}
class Human {
int _strength = 0;
int _energy = 0;
Human(this._strength, this._energy);
void increaseStrength(int value) {
_strength += value;
}
@override
String toString() => "strength: $_strength, energy: $_energy";
}
Mamy siłownię, która potrafi zwiększyć siłę zawodnika oraz klasę człowieka, która posiada dwa pola: siłę oraz energię. Dodatkowo przeciążyłem metodę toString()
, która przyda się nam do sprawdzenia wartości obiektów podczas testowania.
Utwórz w folderze test
(główne drzewo projektu) plik o nazwie gym_test.dart
i umieść w nim następującą treść:
import 'package:equatable_unit_test/gym.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('increaseStrength', () {
Human tHuman = Human(10, 10);
test('should increase human strength', () async {
//arrange
final gym = Gym();
//act
final result = gym.increaseStrength(tHuman, 2);
//assert
expect(result, equals(Human(12, 10)));
});
});
}
Kilka słów wyjaśnienia. Funkcja group
pozwala (jak sama nazwa wskazuje) na grupowanie podobnych sobie testów. Np. kiedy testujesz jedną z metod, warto umieścić takie testy w jednej grupie. Pozwoli Ci to na uporządkowanie kodu, a dodatkowo środowisko przedstawi Ci wynik testów z podziałami właśnie na takie grupy.
Zgrupowane testy jednostkowe.
Test jest podzielony na trzy sekcje:
arrange
— odpowiada za przygotowanie logiki do testów tj. utworzenie obiektów, wprowadzenie odpowiedniego stanu początkowego,act
— tu wykonujemy akcję, której wynik chcemy przetestować,assert
— sprawdzamy wynik, upewniamy się, że testowany kod działa, jak powinien.
Nasz test ma za zadanie sprawdzić, czy siłownia rzeczywiście zwiększa siłę człowieka. Przekonajmy się 🙂
W pierwszym bloku tworzymy obiekt siłowni. Następnie przekazujemy obiekt człowieka siłowni, aby zwiększyć jego siłę o 2 punkty, a wynik działania zwracamy do zmiennej result
której wartość zaraz zbadamy. Jak się domyślasz, funkcja expect()
porównuje dwa obiekty.
Uruchom testy poleceniem:
flutter test
Po chwili otrzymasz następujący rezultat:
00:02 +0 -1: increaseStrength should increase human strength [E]
Expected: Human:<strength: 12, energy: 10>
Actual: Human:<strength: 12, energy: 10>
package:test_api expect
package:flutter_test/src/widget_tester.dart 364:3 expect
gym_test.dart 16:7 main.<fn>.<fn>
00:03 +0 -1: Some tests failed.
Mimo poprawnego zachowania się naszego kodu (obiekt wynikowy posiada odpowiednio zwiększoną wartość siły to test i tak nie przeszedł). No właśnie, domyślne porównywanie…
Czas na…
Biblioteka Equatable
equatable: ^1.1.1
I zaktualizuj kod klasy Human:
class Human extends Equatable {
int _strength = 0;
int _energy = 0;
Human(this._strength, this._energy);
void increaseStrength(int value) {
_strength += value;
}
@override
List<Object> get props => [_strength, _energy];
@override
String toString() => "strength: $_strength, energy: $_energy";
}
To co się zmieniło to dodanie dziedziczenia po klasie Equatable
i przeciążeniu właściwości props
. Do czego ona służy? Jest to szybki sposób na przekazanie listy pól, które mają być brane pod uwagę podczas porównywania obiektów.
Gdybyś zajrzał do wnętrza klasy Equatable
, to znalazłbyś tam przeciążony operator==
:
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
equals(props, other.props);
Widać, że brana jest pod uwagę lista props
. Funkcja equals
zawiera zaawansowane porównywanie tej listy.
Super, ale co z naszym testem? Sprawdźmy to!
Happy end
Wynik testów jest następujący:
00:02 +1: All tests passed!
Piękna sprawa, test przeszedł. Standardowo korzystam z tej biblioteki w swoich projektach, ale oczywiście mógł samodzielnie przeciążyć operator ==, ale czy ma to sens, skoro istnieje rozwiązanie, które robi to w przystępny i klarowny sposób?