mobile

Flutter. Bez Equatable nawet nie zaczynaj testować.

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.

Co spodobało mi się w TDD?
Programistą Być - Programowanie

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.

Zgrupowane testy jednostkowe.

Test jest podzielony na trzy sekcje:

  1. arrange — odpowiada za przygotowanie logiki do testów tj. utworzenie obiektów, wprowadzenie odpowiedniego stanu początkowego,
  2. act — tu wykonujemy akcję, której wynik chcemy przetestować,
  3. 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?

equatable | Dart Package
A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.

Tagi