Testy jednostkowe w F# – przyjazne nazwy

Po kilku latach pisania testów jednostkowych postanowiłem w końcu odejść od MSTest na rzecz nUnit. Powodów jest kilka. MSTest jest nierozwijany od dawna, więc jest daleko w tyle za konkurencją. Brakuje mu np. Assert.Throws. Jasne – oferuje atrybut ExpectedException, ale on sprawia problemy z pokryciem testów i do tego nie wygląda spójnie z innymi asercjami. Nie wspiera różnych parametrów wejściowych. Na sam koniec zaczęło mi przeszkadzać to, że MSTest it wolno odpala testy, co rzuciło mi się w oczy dopiero jak uruchomiłem projekt w VS bez Resharpera. A o wymaganym atrybucie TestClass to wspomnę później.

Zmiana frameworka to jednak nie wszystko, bo postanowiłem też zmienić język pisania testów. Na F#. Dlaczego? Żeby nauczyć się czegoś nowego. Pisanie testów jednostkowych w innym języku wydaje się prostym i bezbolesnym sposobem, żeby się go nauczyć. Dodatkowo można zyskać na czytelności. Ale po kolei.

Oto zrzut okienka Unit Test Sessions z ReSharpera w przypadku, gdy piszemy testy jednostkowe w C# i MSTest:

Testy jednostkowe w C#

Testy jednostkowe w C#

Co tu widzimy? Długą nazwę, w której słowa oddzielone są znakiem podkreślenia. Jest to niby czytelne, jednak nie do końca naturalne. Pewien problem z odczytaniem sprawiają nazwy w stylu „is_time_23_59_59_properly_parsed”. Jak się przyjrzymy to zrozumiemy, że ten test sprawdza czy „godzina 23:59:59 jest prawidłowo sparsowana”. Ale trzeba się przyjrzeć. A gdyby tak test miał nazwę, która pisana jest w języku bliższym do naturalnego, czyli nieograniczonym technologią, którą wykorzystujemy do sporządzenia testu? I tu pojawia się pierwszy plus F#. Poniższy zrzut pokazuje okienko Unit Test Sessions z tymi samymi testami ale napisanymi w F#. Słowa są oddzielone spacjami, a składowe godziny oddzielone dwukropkiem. Idąc dalej – możemy nazwać test jak nam się podoba, używając dowolnych znaków, bo nazwa testu to po prostu ciąg znaków. Fajne, prawda?

Testy jednostkowe w F#

Testy jednostkowe w F#

Poniżej znajduje się fragment kodu, który tworzy testy widziane na pierwszej grafice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace BerlinClock.UnitTests
{
    [TestClass]
    public class TimeUnitTests
    {
        //arrange
        var time = "23:59:59";
 
        //act
        var parsedTime = Time.Parse(time);
 
        //assert
        Assert.AreEqual(23, parsedTime.Hour);
        Assert.AreEqual(59, parsedTime.Minute);
        Assert.AreEqual(59, parsedTime.Second);
    }

Pierwsze próby napisania tego samego testu w F# nie prezentują się lepiej. Ot brak klamr, tylko wcięcia. Jak w Pythonie czy w Ruby.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module TimeUnitTests
 
open Microsoft.VisualStudio.TestTools.UnitTesting;
open BerlinClock.Classes
 
[<TestClass>]
type TimeUnitTests() =
     
    [<TestMethod>]
    member this.is_time_23_59_59_properly_parsed() =
        //arrange
        let time = "23:59:59"
         
        //act
        let parsedTime = Time.Parse(time)
 
        //assert
        Assert.AreEqual(23, parsedTime.Hour)
        Assert.AreEqual(59, parsedTime.Minute)
        Assert.AreEqual(59, parsedTime.Second)

Idąc jednak dalej, możemy otoczyć nazwę w podwójny akcent słaby (tzw. grawis) i zapisać ją w dowolny sposób.

1
2
3
4
5
6
7
8
9
10
11
12
[<TestMethod>]
member this.``is time 23:59:59 properly parsed``() =
    //arrange
    let time = "23:59:59"
     
    //act
    let parsedTime = Time.Parse(time)
 
    //assert
    Assert.AreEqual(23, parsedTime.Hour)
    Assert.AreEqual(59, parsedTime.Minute)
    Assert.AreEqual(59, parsedTime.Second)

Teraz wygląda to lepiej. Mamy obiecaną wcześniej nazwę testu w przyjaznej postaci i ta forma pojawi nam się w okienku z testami. Pełen kod znajduje się na GitHubie.

F# pozwala na przypisanie funkcji bezpośrednio do modułu a nie tylko do klasy. Jednak MSTest nie pozwala na pominięcie atrybutu TestClass, który znajduje się nad deklaracją klasy. Gdy go wyrzucimy to testy nie będą odnajdywane przez test runner. Kolejny powód, żeby zmienić framework. Przechodząc na nUnit, zaraz po zainstalowaniu i zaimportowaniu biblioteki, najpierw zamieniamy atrybuty TestClass na TextFixture oraz TestMethod na Test (GitHub).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[<TestFixture>]
type TimeUnitTests() =
 
    [<Test>]
    member this.``is time 23:59:59 properly parsed``() =
    //arrange
    let time = "23:59:59"
  
    //act
    let parsedTime = Time.Parse(time)
 
    //assert
    Assert.That(parsedTime.Hour, Is.EqualTo(23))
    Assert.That(parsedTime.Minute, Is.EqualTo(59))
    Assert.That(parsedTime.Second, Is.EqualTo(59))

Kolejnym krok to przeniesienie funkcji testowych poza klasę  i przypisanie ich do modułu. Robimy to za pomocą słowa kluczowego let zamiast member this. Niwelujemy też wcięcia, tak, żeby metoda była na tym samym poziomie co deklaracja module. Kodu klasy otoczonego atrybutem TestFixture możemy się już pozbyć (GitHub).

1
2
3
4
5
6
7
8
9
10
11
12
[<Test>]
let ``is time 23:59:59 properly parsed``() =
    //arrange
    let time = "23:59:59"
  
    //act
    let parsedTime = Time.Parse(time)
 
    //assert
    Assert.That(parsedTime.Hour, Is.EqualTo(23))
    Assert.That(parsedTime.Minute, Is.EqualTo(59))
    Assert.That(parsedTime.Second, Is.EqualTo(59))

Teraz kod wydaje się być znacznie bardziej przyjemny i czytelny. Testowy kod czeka jeszcze jedno usprawnienie, które doprowadzi do znacznego zmniejszenia ilości kodu przy zachowaniu tych samych testów. Ale o tym już w następnym wpisie.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

*
*
Website