Czy istnieje lepszy sposób pisania testów jednostkowych niż seria „AssertEquals”?

12

Oto podstawowy przykład tego, czym powinien być mój test jednostkowy, za pomocą qunit:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>

<link rel="stylesheet" href="qunit/qunit-1.13.0.css">
<script src = "qunit/qunit-1.13.0.js"></script>
<script src = "../js/fuzzQuery.js"></script>

<script>

test("Fuzz Query Basics", function()
        {
            equal(fuzzQuery("name:(John Smith)"), "name:(John~ Smith~)");
            equal(fuzzQuery("name:Jon~0.1"), "name:Jon~0.1");
            equal(fuzzQuery("Jon"), "Jon~");
            //etc

        }
    );

</script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Teraz myślałem, że to trochę powtarzalne.

Można umieścić wszystkie wejścia / wyjścia w tablicy i przejść przez nią.

test("Fuzz Query Basics", function()
        {
            var equals = [
                           ["name:(John Smith)", "name:(John~ Smith~)"],
                           ["name:Jon~0.1", "name:Jon~0.1"],
                           ["Jon", "Jon~"]
                           ];

            for (var i = 0; i<equals.length; i++)
                {
                    equal(fuzzQuery(equals[i][0]), equals[i][1]);               
                }

        }
    );

I to działa dobrze.

Jedyną zaletą tej drugiej metody, jaką mogę wymyślić, jest to, że jeśli okaże się, że tak naprawdę nie chcesz jej używać equal, łatwiej jest dokonać tej zmiany w jednym miejscu.

Jeśli chodzi o czytelność, nie sądzę, że jest to jednoznaczne, chociaż prawdopodobnie wolę drugą.

Abstrahując go dalej, możesz umieścić przypadki wejścia / wyjścia w osobnym pliku CSV, co może ułatwić modyfikację.

Pytanie brzmi - jakie są ogólne konwencje dotyczące pisania tego rodzaju testów jednostkowych?

Czy istnieje powód, dla którego nie powinieneś umieszczać ich w tablicach?

dwjohnston
źródło
Czy któryś z nich powie ci, która wartość zawiodła?
JeffO
1
@JeffO - Tak - Z QUnit - Jeśli test się nie powiedzie, na wyjściu pojawi się wartość oczekiwana i rzeczywista.
dwjohnston

Odpowiedzi:

8

Twoje refaktoryzowane testy mają zapach: Logika testów warunkowych .

Powody, dla których należy unikać pisania logiki warunkowej w testach, są dwojakie. Po pierwsze, podważa to twoją zdolność do upewnienia się, że kod testowy jest poprawny, jak opisano w połączonym artykule Wzory xUnit.

Drugi polega na tym, że przesłania on znaczenie testów. Piszemy Metody testowe, ponieważ umieszczają logikę testowania danego zachowania w jednym miejscu i pozwalają nam nadać mu opisową nazwę (zobacz oryginalny artykuł BDD Dana Northa , aby poznać wartość dobrych nazw dla testów). Gdy testy są ukryte w jednej funkcji z forpętlą, przesłania to znaczenie kodu dla czytnika. Czytelnik nie tylko musi zrozumieć pętlę, ale także mentalnie rozwikłać wszystkie zachowania badane w pętli.

Rozwiązaniem, jak zawsze, jest podniesienie poziomu abstrakcji. Użyj frameworka testowego, który daje sparametryzowane testy , takie jak xUnit.NET lub Contexts do (zrzeczenie się: Napisałem Contexts). Umożliwia to grupowanie testów triangulacyjnych dla tego samego zachowania razem w naturalny sposób, przy jednoczesnym zachowaniu osobnych testów dla osobnych zachowań.

Benjamin Hodgson
źródło
Przy okazji, dobre pytanie
Benjamin Hodgson
1
1) Jeśli przejdziesz na wyższy poziom abstrakcji, czy nie ukrywasz tych samych szczegółów, o których mówiłeś, że są zaciemnione przez pętlę for? 2) nie jestem pewien, czy mają tu zastosowanie sparametryzowane testy. Wygląda na to, że gdzieś tu są podobieństwa, ale miałem wiele sytuacji podobnych do PO, w których miałem zestaw danych 10-20 wartości i chciałem je wszystkie uruchomić przez SUT. Tak, każda wartość jest inna i potencjalnie testuje różne buduny, ale wydaje się, że tak naprawdę „wymyślenie” nazw testów dla każdej pojedynczej wartości byłoby przesadą. Znalazłem optymalny stosunek wartości do kodu przy użyciu podobnego ...
DXM
... pętle. Tak długo, jak test się nie powiedzie, aser drukuje dokładnie to, co się nie powiodło, deweloper ma wystarczającą liczbę opinii, aby precyzyjnie wskazać problem.
DXM
@DXM 1) środowisko testowe zapewnia sparametryzowaną funkcjonalność testową. Ufamy strukturze testów pośrednio, więc nie piszemy dla niej testów. 2) sparametryzowane testy są właśnie w tym celu: za każdym razem wykonujesz dokładnie te same kroki, ale z różnymi wartościami wejściowymi / wyjściowymi. Środowisko testowe oszczędza konieczności pisania nazw dla każdego z nich, uruchamiając różne dane wejściowe za pomocą tej samej metody testowej.
Benjamin Hodgson
5

Wygląda na to, że naprawdę chcesz przeprowadzić test jednostkowy oparty na danych. Ponieważ wspomniałeś o użyciu QUnit, znalazłem wtyczkę, która umożliwia sparametryzowane testy:

https://github.com/AStepaniuk/qunit-parameterize

Nie ma nic ideologicznie złego w teście opartym na danych, o ile sam kod testu nie jest warunkowy. Patrząc na kod testowy, wydaje się być bardzo dobrym kandydatem do testu opartego na danych.

Przykładowy kod GADHub README:

QUnit
    .cases([
        { a : 2, b : 2, expectedSum : 4 },
        { a : 5, b : 5, expectedSum : 10 },
        { a : 40, b : 2, expectedSum : 42 }
    ])
    .test("Sum test", function(params) {
        var actualSum = sum(params.a, params.b);
        equal(actualSum, params.expectedSum);
    });
Greg Burghardt
źródło
1
Uzgodniony, wygląda na test oparty na danych. Ale wygląda na to, że ma to już w swoim drugim przykładzie kodu.
Robert Harvey
1
@RobertHarvey - poprawnie. Istnieje akceptowany termin określający to, co próbuje osiągnąć, i istnieje wtyczka do struktury testowania, która ułatwia pisanie tego rodzaju testów. Pomyślałem, że warto zauważyć w odpowiedzi na przyszłość, to wszystko.
Greg Burghardt
1

Powtarzasz się mniej, używając tablicy, która jest łatwiejsza w utrzymaniu. Jednym z podejść, które lubię stosować, jest oddzielna metoda, która porządkuje, wykonuje i potwierdza testy, ale akceptuje parametry wejściowe, które testuję, więc mam 1 metodę testową na zestaw danych wejściowych.

To pozwala mi natychmiast stwierdzić, które testy / wejścia zawodzą.

Kevin
źródło
0

Podoba mi się twoje drugie podejście, ale dodałbym 2 punkty

  • nie używaj tablic do przechowywania testowanych danych, ponieważ praca z indeksami nie jest czystym sposobem
  • nie używaj forpętli

`

[
    {
        process: "name:(John Smith)",
        result: "name:(John~ Smith~)"
    },
    {
        process: "name:Jon~0.1", 
        result: "name:Jon~0.1"
    },
    {
        process: "Jon", 
        result: "Jon~"
    }
]
.forEach(function(data){

    var result = fuzzQuery(data.process);
    equal(result, data.result);
});

Nie jestem pewien co do qunit, ale dobry tester pokaże, który łańcuch wejściowy zawiódł i jaki był oczekiwany wynik

tenbity
źródło