Jakiś czas temu miałem przyjemność pisać webową aplikację w stylu SPA.
Wybór technologii zakończył się na JavaScript + AngularJs + WebAPI.
Nic nowego, standardowy stos i aplikacja…
Jednakże, dla mnie było to pierwsze spotkanie z testami jednostkowymi w JavaScriptcie.
A dokładniej z frameworkiem: Jasmine
Jasmine zowie się frameworkiem testowym o smaku Behavior-Driven.
Wygląda to mniej więcej tak:
describe("Validator", function () { var input; var validator; beforeEach(function () { input = {}; validator = new Validator(); }); describe("when email do not exist", function () { it("validation fails", function () { expect(validator.validate(input)).toEqual({ isSuccess: false, errorMessage: 'Missing email' }); }); }); describe("when email exist", function () { beforeEach(function () { input.email = '' }); describe("when email is empty", function () { it("validation fails", function () { expect(validator.validate(input)).toEqual({ isSuccess: false, errorMessage: 'Empty email' }); }); }); describe("when email has incorrect format", function () { beforeEach(function () { input.email = 'incorrect-format' }); it("validation fails", function () { expect(validator.validate(input)).toEqual({ isSuccess: false, errorMessage: 'Incorrect email' }); }); }); describe("when email has correct format", function () { beforeEach(function () { input.email = 'correct@email.com' }); it("validation succeeds", function () { expect(validator.validate(input)).toEqual({ isSuccess: true }); }); }); }); });
A co w tym jest takiego ciekawego?
A no, pomysł na zagnieżdżanie testów.
Hierarchia w testach
Jeżeli mamy kod który posiada strukturę drzewiastą (np. walidacja) to często będziemy powtarzać etap przygotowania stanu przed samym testem.
Spójrzmy na standardowy przykład testów w C#:
public class ValidatorTests { private Validator _validator; [SetUp] public void SetUp() { _validator = new Validator(); } [Test] public void WhenEmailIsNull_ValidationFails() { string email = null; var result = _validator.Validate(email); Assert.IsFalse(result.IsSuccess); } [Test] public void WhenEmailIsEmpty_ValidationFails() { string email = ""; var result = _validator.Validate(email); Assert.IsFalse(result.IsSuccess); } [Test] public void WhenEmailHasIncorrectFormat_ValidationFails() { string email = "Incorrect"; var result = _validator.Validate(email); Assert.IsFalse(result.IsSuccess); } [Test] public void WhenEmailHasCorrectFormat_ValidationSucceeds() { string email = "correct@email.com"; var result = _validator.Validate(email); Assert.IsTrue(result.IsSuccess); } }
a teraz na wersję z zagnieżdżeniem (warto zwrócić uwagę na hierarchię dziedziczenia):
[TestFixture] public class ValidatorTests { private string _email = null; private Validator _validator; [SetUp] public void SetUp() { _validator = new Validator(); } [TestFixture] public class WhenEmailIsNull : ValidatorTests { [Test] public void ValidationFails() { var result = _validator.Validate(_email); Assert.IsFalse(result.IsSuccess); } } [TestFixture] public class WhenEmailIsEmpty : ValidatorTests { [SetUp] public void SetUp() { _email = ""; } [Test] public void ValidationFails() { var result = _validator.Validate(_email); Assert.IsFalse(result.IsSuccess); } } [TestFixture] public class WhenEmailIsNotEmpty : ValidatorTests { [TestFixture] public class WhenEmailHasIncorrectFormat : WhenEmailIsNotEmpty { [SetUp] public void SetUp() { _email = "incorrect"; } [Test] public void ValidationFails() { var result = _validator.Validate(_email); Assert.IsFalse(result.IsSuccess); } } [TestFixture] public class WhenEmailHasCorrectFormat : WhenEmailIsNotEmpty { [SetUp] public void SetUp() { _email = "correct@email.com"; } [Test] public void ValidationSucceeds() { var result = _validator.Validate(_email); Assert.IsTrue(result.IsSuccess); } } } }
Porównajmy również raport z R#:
Oczywiście, im więcej mamy rozgałęzień w logice oraz im bardziej skomplikowany kod w środku – tym więcej będziemy w stanie zmieścić w zagnieżdżonych setupach.
Kiedy stosować?
- Metody setup zaczynają przybierać na liniach
- Struktura kodu przypomina drzewo
- Kiedy w metodach testów pojawiają się słowa typu: “when”, “then” itp
- Kiedy chcemy mieć bardziej czytelne kolejne kroki algorytmów (w testach)
Inne przypadki
Testy integracyjne często układają się w drzewo-podobne struktury.
Np. w testach zapytań SQL – Join z kilku tabelek zwykle wygląda tak:
SELECT TableA.*, TableB.*, TableC.* FROM TableA JOIN TableB ON TableB.aID = TableA.aID JOIN TableC ON TableC.cID = TableB.cID
Więc nasze drzewo testów będzie miało formę:
- When records in table A do not exist
- Query returns empty list
- When records in table A do exist
- When records in table B do not exist
- Query returns empty list
- When records in table B do exist
- When records in table C do not exist
- Query returns empty list
- When records in table C do exist
- Query returns selected records
- When records in table C do not exist
- When records in table B do not exist
Pisanie metody pokroju:
WhenTableAHasRecords_And_TableBHasRecords_And_TableCHasRecords_RecordsAreReturned
raczej nie należy do najprzyjemniejszych, a próba jej przeczytania przez innego programistę prędzej skończy się na tępych odgłosach konsultacji głowa-biurko niż słowach afirmacji.
Be First to Comment