Czy oczekiwane wyniki testu jednostkowego powinny być zakodowane na stałe?

29

Czy oczekiwane wyniki testu jednostkowego powinny być zakodowane na stałe, czy też mogą zależeć od zainicjowanych zmiennych? Czy wyniki zapisane na stałe lub obliczone zwiększają ryzyko wprowadzenia błędów w teście jednostkowym? Czy są jeszcze inne czynniki, których nie wziąłem pod uwagę?

Na przykład, który z tych dwóch formatów jest bardziej niezawodny?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDYCJA 1: Czy w odpowiedzi na odpowiedź DXM opcja 3 jest preferowanym rozwiązaniem?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Hand-E-Food
źródło
2
Zrób jedno i drugie. Poważnie. Testy mogą i powinny się nakładać. Zajrzyj także do testów opartych na danych, jeśli masz do czynienia z zakodowanymi wartościami.
Job
Zgadzam się, że trzecią opcją jest to, co lubię używać. Nie sądzę, by opcja 1 zaszkodziła, ponieważ wyeliminowałeś manipulację podczas kompilacji.
kwelch
Obie opcje używają jednak
twardego kodowania i ulegną awarii,

Odpowiedzi:

27

Myślę, że obliczona oczekiwana wartość daje bardziej solidne i elastyczne przypadki testowe. Również poprzez użycie dobrych nazw zmiennych w wyrażeniu, które obliczają oczekiwany wynik, jest o wiele bardziej jasne, skąd ten oczekiwany wynik pochodzi w pierwszej kolejności.

Powiedziawszy to, w twoim konkretnym przykładzie NIE ufam metodzie „Softcoded”, ponieważ używa ona twojego SUT (testowanego systemu) jako danych wejściowych do twoich obliczeń. Jeśli w MyClass występuje błąd, w którym pola nie są poprawnie przechowywane, test faktycznie się powiedzie, ponieważ podczas obliczania oczekiwanej wartości użyjemy niewłaściwego ciągu, podobnie jak target.GetPath ().

Moją sugestią byłoby obliczenie oczekiwanej wartości tam, gdzie ma to sens, ale upewnij się, że obliczenie nie zależy od żadnego kodu z samego SUT.

W odpowiedzi na aktualizację OP na moją odpowiedź:

Tak, w oparciu o moją wiedzę, ale nieco ograniczone doświadczenie w wykonywaniu TDD, wybrałbym opcję nr 3.

DXM
źródło
1
Słuszna uwaga! Nie polegaj na niezweryfikowanym obiekcie w teście.
Hand-E-Food,
czy to nie jest powielanie kodu SUT?
Abyx,
1
w pewnym sensie tak, ale w ten sposób weryfikujesz, czy SUT działa. Gdybyśmy użyli tego samego kodu i został on złamany, nigdy byś nie wiedział. Oczywiście, jeśli w celu wykonania obliczeń trzeba zduplikować dużo SUT, być może opcja nr 1 stałaby się lepsza, po prostu zakodowała wartość.
DXM,
16

Co jeśli kod był następujący:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Twój drugi przykład nie złapie błędu, ale pierwszy przykład.

Ogólnie rzecz biorąc, odradzam programowanie miękkie, ponieważ może ukrywać błędy. Na przykład:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Czy potrafisz dostrzec problem? Nie popełniłbyś tego samego błędu w wersji zakodowanej na stałe. Trudniej jest uzyskać poprawne obliczenia niż wartości zakodowane na stałe. Dlatego wolę pracować z wartościami zakodowanymi na stałe niż z kodami programowanymi.

Ale są wyjątki. Co zrobić, jeśli Twój kod musi działać w systemie Windows i Linux? Ścieżka musi być nie tylko inna, ale musi także używać różnych separatorów ścieżek! Obliczanie ścieżki za pomocą funkcji, które wyodrębniają różnicę między, może mieć sens w tym kontekście.

Winston Ewert
źródło
Słyszę, co mówisz, i to daje mi coś do rozważenia. Softcoding opiera się na moich innych testowych przypadkach (takich jak ConstructorShouldCorrectlyInitialiseFields). Opisana przez ciebie awaria zostałaby powiązana z innymi niepowodzeniami innych testów jednostkowych.
Hand-E-Food
@ Hand-E-Food, brzmi to tak, jakbyś pisał testy poszczególnych metod swoich obiektów. Nie rób Powinieneś pisać testy, które sprawdzają poprawność całego obiektu razem, a nie poszczególne metody. W przeciwnym razie twoje testy będą kruche w odniesieniu do zmian wewnątrz obiektu.
Winston Ewert,
Nie jestem pewien, czy podążam. Podany przeze mnie przykład był czysto hipotetyczny, łatwy do zrozumienia. Piszę testy jednostkowe, aby przetestować publicznych członków klas i obiektów. Czy to właściwy sposób na ich użycie?
Hand-E-Food,
@ Hand-E-Food, jeśli dobrze cię rozumiem, twój test ConstructShouldCorrectlyInitialiseFields wywołałby konstruktor, a następnie stwierdził, że pola są ustawione poprawnie. Ale nie powinieneś tego robić. Nie powinieneś przejmować się tym, co robią pola wewnętrzne. Należy jedynie stwierdzić, że zachowanie zewnętrzne obiektu jest prawidłowe. W przeciwnym razie może nadejść dzień, w którym konieczne będzie zastąpienie wewnętrznej implementacji. Jeśli poczyniłeś twierdzenia o stanie wewnętrznym, wszystkie testy się zepsują. Ale jeśli poczyniłeś tylko twierdzenia o zachowaniu zewnętrznym, wszystko nadal będzie działać.
Winston Ewert,
@ Winston - Właśnie przeszukuję książkę xUnit Test Patterns i wcześniej skończyłem The Art of Unit Testing. Nie zamierzam udawać, że wiem, o czym mówię, ale chciałbym myśleć, że coś wybrałem z tych książek. Obie książki zdecydowanie zalecają, aby każda metoda testowa przetestowała absolutne minimum, a ty powinieneś mieć wiele przypadków testowych do przetestowania całego obiektu. W ten sposób, gdy zmieniają się interfejsy lub funkcje, należy spodziewać się naprawienia tylko kilku metod testowych, a nie większości z nich. A ponieważ są małe, zmiany powinny być łatwiejsze.
DXM,
4

Moim zdaniem obie twoje sugestie są mniej niż idealne. Idealny sposób to zrobić:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Innymi słowy, test powinien działać wyłącznie na podstawie danych wejściowych i wyjściowych obiektu, a nie na podstawie stanu wewnętrznego obiektu. Obiekt należy traktować jak czarną skrzynkę. (Nie lekceważę innych problemów, takich jak niewłaściwość użycia łańcucha.Join zamiast Path.Combine, ponieważ jest to tylko przykład.)

Mike Nakis
źródło
1
Nie wszystkie metody działają - wiele poprawnie ma skutki uboczne, które zmieniają stan niektórych obiektów lub obiektów. Test jednostkowy dla metody z efektami ubocznymi prawdopodobnie wymagałby oceny stanu obiektu (ów), na który wpływa ta metoda.
Matthew Flynn,
Wtedy ten stan byłby uważany za wynik metody. Celem tego przykładowego testu jest sprawdzenie metody GetPath (), a nie konstruktora MyClass. Przeczytaj odpowiedź @ DXM, stanowi on bardzo dobry powód do przyjęcia podejścia czarnej skrzynki.
Mike Nakis,
@MatthewFlynn, powinieneś przetestować metody, których dotyczy ten stan. Dokładny stan wewnętrzny jest szczegółem implementacji i nie jest przedmiotem działalności testu.
Winston Ewert,
@MatthewFlynn, aby wyjaśnić, czy ma to związek z pokazanym przykładem, czy może jest czymś innym do rozważenia w przypadku innych testów jednostkowych? Widziałem, że ma to znaczenie dla czegoś takiego target.Dispose(); Assert.IsTrue(target.IsDisposed);(bardzo prosty przykład)
Hand-E-Food
Nawet w tym przypadku właściwość IsDisposed jest (lub powinna być) niezbędną częścią publicznego interfejsu klasy, a nie szczegółem implementacji. (Interfejs IDispose nie zapewnia takiej właściwości, ale to niefortunne.)
Mike Nakis,
2

Dyskusja obejmuje dwa aspekty:

1. Używanie samego obiektu docelowego w przypadku testowym
Pierwsze pytanie brzmi: czy należy użyć samej klasy, aby polegać i wykonywać część pracy w testowym odcinku testowym? - Odpowiedź brzmi NIE, ponieważ ogólnie rzecz biorąc, nigdy nie powinieneś zakładać się o testowany kod. Jeśli nie zostanie to wykonane poprawnie, z czasem błędy stają się odporne na niektóre testy jednostkowe.

2. Twarde kodowanie
Czy powinieneś ciężko kodować ? Ponownie odpowiedź brzmi: nie . ponieważ jak każde oprogramowanie - twarde kodowanie informacji staje się trudne, gdy rzeczy ewoluują. Na przykład, jeśli chcesz ponownie zmodyfikować powyższą ścieżkę, musisz napisać dodatkową jednostkę lub kontynuować modyfikację. Lepszym sposobem jest utrzymanie daty wprowadzania i oceny na podstawie oddzielnej konfiguracji, którą można łatwo dostosować.

na przykład tutaj chciałbym skorygować kod testowy.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Dipan Mehta
źródło
0

Istnieje wiele możliwych koncepcji, podanych kilka przykładów, aby zobaczyć różnicę

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Podsumowując: Ogólnie rzecz biorąc, twój pierwszy właśnie zakodowany test ma dla mnie sens, ponieważ jest prosty, od razu do rzeczy itp. Jeśli zbyt wiele razy zaczniesz kodować ścieżkę, po prostu umieść ją w metodzie konfiguracji.

Aby uzyskać więcej przyszłych testów strukturalnych, poszedłbym sprawdzić źródła danych, abyś mógł dodać więcej wierszy danych, jeśli potrzebujesz więcej sytuacji testowych.

Luc Franken
źródło
0

Nowoczesne frameworki testowe umożliwiają dostarczanie parametrów do metody. Wykorzystałbym te:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Moim zdaniem ma to kilka zalet:

  1. Programiści często mają pokusę kopiowania pozornie prostych części kodu z SUT do swoich testów jednostkowych. Jak zauważa Winston , w tych nadal mogą być ukryte trudne błędy. „Zakodowane na stałe” oczekiwany wynik pomaga uniknąć sytuacji, w których kod testowy jest nieprawidłowy z tego samego powodu, dla którego oryginalny kod jest nieprawidłowy. Ale jeśli zmiana wymagań zmusi cię do wyśledzenia na stałe napisów wbudowanych w dziesiątki metod testowych, może to być denerwujące. Posiadanie wszystkich zakodowanych wartości w jednym miejscu, poza logiką testowania, daje to, co najlepsze z obu światów.
  2. Możesz dodać testy dla różnych danych wejściowych i oczekiwanych wyników za pomocą jednego wiersza kodu. To zachęca do napisania większej liczby testów, przy jednoczesnym zachowaniu SUCHEGO kodu testowego i łatwym w utrzymaniu. Uważam, że ponieważ dodawanie testów jest tak tanie, mój umysł jest otwarty na nowe przypadki testowe, o których nie myślałbym, gdybym musiał napisać dla nich zupełnie nową metodę. Na przykład, czego bym się spodziewał, gdyby jedno z wejść zawierało kropkę? Odwrotny ukośnik? Co jeśli ktoś był pusty? A może biały znak? Lub zaczął się lub zakończył spacją?
  3. Struktura testowa potraktuje każdy TestCase jako własny test, nawet umieszczając dostarczone dane wejściowe i wyjściowe w nazwie testu. Jeśli wszystkie TestCases przejdą, ale jeden, to bardzo łatwo zobaczyć, który z nich się zepsuł i jak różni się od pozostałych.
StriplingWarrior
źródło