Chciałem nauczyć się korzystać z podejścia TDD i miałem projekt, nad którym chciałem pracować od dłuższego czasu. To nie był duży projekt, więc pomyślałem, że będzie dobrym kandydatem do TDD. Czuję jednak, że coś poszło nie tak. Podam przykład:
Na wysokim poziomie mój projekt jest dodatkiem do Microsoft OneNote, który pozwoli mi łatwiej śledzić i zarządzać projektami. Teraz chciałem zachować logikę biznesową w tym zakresie możliwie jak najbardziej oddzieloną od OneNote, na wypadek, gdyby któregoś dnia postanowiłem zbudować własną pamięć masową i zaplecze.
Najpierw zacząłem od podstawowego testu akceptacji prostych słów, który nakreślił, co chciałbym, aby moja pierwsza funkcja była zrobiona. Wygląda to mniej więcej tak:
- Kliknięcia użytkownika tworzą projekt
- Typy użytkowników w tytule projektu
- Sprawdź, czy projekt został poprawnie utworzony
Pomijam interfejs użytkownika i trochę planowania pośredniego, przechodzę do pierwszego testu jednostkowego:
[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
var testController = new Controller();
Project newProject = testController(A.Dummy<String>());
Assert.IsNotNull(newProject);
}
Jak na razie dobrze. Czerwony, zielony, refaktor itp. W porządku, teraz potrzebuje rzeczy. Wycinając tutaj kilka kroków, kończę z tym.
[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
var fakeDataStore = A.Fake<IDataStore>();
var testController = new Controller(fakeDataStore);
String expectedTitle = fixture.Create<String>("Title");
Project newProject = testController(expectedTitle);
Assert.AreEqual(expectedTitle, newProject.Title);
}
W tym momencie nadal czuję się dobrze. Nie mam jeszcze konkretnego magazynu danych, ale stworzyłem interfejs tak, jak się spodziewałem.
Pominę tutaj kilka kroków, ponieważ ten post jest wystarczająco długi, ale postępowałem zgodnie z podobnymi procesami i ostatecznie przechodzę do tego testu dla mojego magazynu danych:
[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
/* snip init code */
testDataStore.SaveNewProject(A.Dummy<IProject>());
A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}
To było dobre, dopóki nie spróbowałem go zaimplementować:
public String SaveNewProject(IProject project)
{
Page projectPage = oneNoteInterop.CreatePage(...);
}
I jest problem właśnie tam, gdzie jest „...”. W tym momencie zdaję sobie sprawę, że CreatePage wymaga identyfikatora sekcji. Nie zdawałem sobie z tego sprawy, kiedy myślałem na poziomie kontrolera, ponieważ byłem zainteresowany jedynie testowaniem bitów istotnych dla kontrolera. Jednak przez całą drogę zdaję sobie sprawę, że muszę poprosić użytkownika o miejsce do przechowywania projektu. Teraz muszę dodać identyfikator lokalizacji do magazynu danych, następnie dodać jeden do projektu, następnie dodać jeden do kontrolera i dodać go do WSZYSTKICH testów, które są już napisane dla wszystkich tych rzeczy. Szybko stało się to żmudne i nie mogę się powstrzymać, ale czuję, że złapałbym to szybciej, gdyby naszkicowałem projekt z wyprzedzeniem, zamiast pozwolić mu zaprojektować go podczas procesu TDD.
Czy ktoś może mi wyjaśnić, czy w tym procesie zrobiłem coś złego? Czy można zrezygnować z tego rodzaju refaktoryzacji? Czy to jest powszechne? Jeśli jest to powszechne, czy są jakieś sposoby na uczynienie go bardziej bezbolesnym?
Dziękuje wszystkim!
źródło
Odpowiedzi:
Chociaż TDD jest (słusznie) reklamowane jako sposób projektowania i rozwijania oprogramowania, nadal dobrze jest wcześniej pomyśleć o projekcie i architekturze. IMO „szkicowanie projektu z wyprzedzeniem” to uczciwa gra. Często jednak będzie to na wyższym poziomie niż decyzje projektowe, do których doprowadzi Cię TDD.
Prawdą jest również to, że gdy coś się zmienia, zwykle będziesz musiał zaktualizować testy. Nie ma sposobu na całkowite wyeliminowanie tego, ale możesz zrobić kilka rzeczy, aby Twoje testy były mniej kruche i zminimalizować ból.
W miarę możliwości trzymaj szczegóły implementacji poza testami. Oznacza to jedynie przeprowadzanie testów metodami publicznymi i, tam gdzie to możliwe, faworyzowanie weryfikacji opartej na stanie zamiast weryfikacji opartej na interakcji . Innymi słowy, jeśli testujesz wynik czegoś, a nie kroki, aby się tam dostać, twoje testy powinny być mniej kruche.
Zminimalizuj duplikację w kodzie testowym, tak jak w kodzie produkcyjnym. Ten post jest dobrym źródłem informacji. W twoim przykładzie wydaje się, że dodanie
ID
właściwości do konstruktora było bolesne, ponieważ wywołałeś konstruktor bezpośrednio w kilku różnych testach. Zamiast tego spróbuj wyodrębnić utworzenie obiektu do metody lub zainicjować go raz dla każdego testu w metodzie inicjalizacji testu.źródło
Może, może nie
Z jednej strony TDD działało dobrze, dając automatyczne testy podczas budowania funkcjonalności i natychmiast przerywając, gdy trzeba było zmienić interfejs.
Z drugiej strony, być może, gdybyś zaczął od funkcji wysokiego poziomu (SaveProject) zamiast funkcji niższego poziomu (CreateProject), wcześniej zauważyłbyś brakujące parametry.
Z drugiej strony, być może nie miałbyś. To niepowtarzalny eksperyment.
Ale jeśli szukasz lekcji na następny raz: zacznij od góry. I najpierw pomyśl o projekcie, ile chcesz.
źródło
https://frontendmasters.com/courses/angularjs-and-code-testability/ Od około 2:22:00 do końca (około 1 godziny). Przepraszam, że film nie jest darmowy, ale nie znalazłem darmowego, który wyjaśnia go tak dobrze.
Jedna z najlepszych prezentacji pisania testowalnego kodu znajduje się w tej lekcji. Jest to klasa AngularJS, ale część testowa dotyczy całego kodu Java, przede wszystkim dlatego, że to, o czym mówi, nie ma nic wspólnego z językiem, a przede wszystkim z pisaniem dobrego testowalnego kodu.
Magią jest pisanie testowalnego kodu, a nie pisanie testów kodu. Nie chodzi o pisanie kodu, który udaje użytkownika.
Poświęca także trochę czasu na napisanie specyfikacji w formie twierdzeń testowych.
źródło