Czy to jest jeszcze test jednostkowy czy już integracyjny?

0

Mam zagwozdkę, czy to wciąż jest test jednostkowy? A mianowicie testuję utworzenie pliku, tak więc wkraczam tu na niebezpieczny teren IO. Teoretycznie nie ma tu nic co by mogło z zewnątrz wpłynąć na wynik testu. W czasie testu sprawdzam czy metoda zapisuje plik z odpowiednią nazwą i czy przy okazji jest wołana inna metoda. Test wygląda następująco:

[Fact]
        public void SavePerson_Called_SavesFileWithCorrectNameAndGestCallerName()
        {
            _fileSerice.CallerName = "";
            string folder = Path.Combine("App_Data", "xml");
            _fileSerice.SaveFolder = folder;
            string filePath = Path.Combine(folder, _personModel.ContentModel.dateRequested);
            if (!Directory.Exists(folder))
                Directory.CreateDirectory(folder);
            if (File.Exists(filePath))
                File.Delete(filePath);

            _fileSerice.SavePerson(_xmlPerson, _personModel);

            Assert.True(File.Exists(filePath));
            Assert.True(filePath.Length > 0);
            Assert.Equal("SomeMethod", _fileSerice.CallerName);
            _messageServiceMock.Verify(m => m.GetCallerName(It.IsAny<string>()), Times.Once);
        }

Testowana metoda:

public void SavePerson(string xmlPerson, PersonModel parsedPerson)
        {
            LoadDictionaries();
            CallerName = _messageService.GetCallerName();

            try
            {
                XmlDocument xmlFile = new XmlDocument();

                xmlFile.LoadXml(xmlPerson);

                string savePath = Path.Combine(SaveFolder, parsedPerson.ContentModel.dateRequested);

                using (TextWriter writer = new StreamWriter(savePath, false, Encoding.UTF8))
                {

                    xmlFile.Save(writer);
                }
            }
            catch (Exception e)
            {
                _logger.Error(e, MessagesError[CallerName]);
            }
        }
2

Jeśli pytasz to znaczy, że jest integracyjny.
No bobra może przesadzam. Ten test jest nieudaną wersją testu jednostkowego

Test jednostkowy powinien

  • testować tylko jeden moduł (bibliotekę) aplikacji
  • zależności powinny być zastąpione mock-ami
  • test, który trwa dłużej niż mrugnięcie okiem, nie kwalifikacje się na miano testu jednostkowego (poza ekstremalnymi wyjątkami, gdzie logika biznesowa ma dużą złożoność obliczeniową)
  • test jednostkowy nie powinien cokolwiek: zapisywać, wysyłać, odbierać, zmieniać w systemie.
0
MarekR22 napisał(a):

Jeśli pytasz to znaczy, że jest integracyjny.
No bobra może przesadzam. Ten test jest nieudaną wersją testu jednostkowego

No dobra, to da się w ogóle napisać test jednostkowy dla metody zapisującej plik? I czemu wg Ciebie to jest nieudana wersja testu jednostkowego? A może po prostu nie trzeba tego testować bo nie ma tu logiki, tylko korzystam z bibliotek ogólnie dostępnych?

0
Charles_Ray napisał(a):

Test dotykający I/O ciężko nazwać jednostkowym: https://github.com/ghsukumar/SFDC_Best_Practices/wiki/F.I.R.S.T-Principles-of-Unit-Testing

Niestety masz rację. Znalazłem coś takiego, może zastosuję, dla sztuki: https://stackoverflow.com/a/1528151/8532173

1

po co się przejmować czy test jest testem a/b/c/d czy e?

1
bakunet napisał(a):
MarekR22 napisał(a):

Jeśli pytasz to znaczy, że jest integracyjny.
No bobra może przesadzam. Ten test jest nieudaną wersją testu jednostkowego

No dobra, to da się w ogóle napisać test jednostkowy dla metody zapisującej plik? I czemu wg Ciebie to jest nieudana wersja testu jednostkowego? A może po prostu nie trzeba tego testować bo nie ma tu logiki, tylko korzystam z bibliotek ogólnie dostępnych?

Oczywiście, że się da. Trzeba jednak zrozumieć co jest twoim kodem, a co zewnętrznym i uwzględnić to jak zależność.
W twoim przypadku zapis xml-a jest zewnętrzną zależnością, którą najlepiej schować za abstrakcją, a w testach wstawić tam mock-a.

Ja bym poprawił kod tak (disclaimer: jestem bardziej C++):

interface IDocSaver
{
void save(XmlDocument doc, string file);
}
...

public IDocSaver docSaver = dependencyInjectionContainer(...);

public string savePath() {
     ...
}

public void SavePerson(Person person) {
            XmlDocument xmlDoc  = new XmlDocument();
            storePersonIntoXml(person, xmlDoc);
            xmlDoc();
            docSaver->save(xmlDoc, savePath());
        }

W teście weryfikowałbym, że:

  • IDocSaver.save zostało wywołane
  • parametr doc dla IDocSaver.save zawiera dane zgodne z danymi trzymanymi w Person
  • parametr file wskazuje na oczekiwaną ścieżkę.

Do tego test sprawdzający co się stanie jak IDocSaver.save żuci wyjątkiem, że coś poszło nie tak z zapisywaniem danych.

Przy czym mam kilka wątpliwości co do twojego testu kodu:

  • wygląda na to, że robisz błędne założenie, że każda klasa ma mieć swoje testy. Wiem, że często tak ludzie uczą pisać testy (sam byłem tak uczony), ale to jest złe podejście. Testy mają odzwierciedlać wymagania logiki biznesowej, a to jaka jest struktura kodu produkcyjnego nie powinno podlegać testom. Testy nie powinny blokować możliwości przepisywania/poprawiania kodu. Nie znam twoich wymagań więc nie potrafię ci pokazać jak to powinno wyglądać.
  • dziwne jest to, że masz argument string xmlPerson i potem robisz xmlFile.LoadXml(xmlPerson); i jeszcze potem xmlFile.Save(writer);. To wygląda jakby ktoś pomieszał eksternalizację z internalizację danych, dla mnie jest "code smell". Dlatego przerobiłem nieco ten przykład.
1

To, co proponujesz to overmocking. Jaki jest sens sprawdzania, czy metoda repozytorium została wywołana i czy jej parametr ma dobrze ustawione pola. Tego nie da się później refactorować. Testuj zachowania, a nie implementację.

Repozytorium można podstawić fejkowe, zrobić zapis na komponencie wyżej i potem odczyt. Asercje dopiero na tej zwrotce.

Wyjątkiem jest sytuacja, kiedy nie kontrolujesz tego docSavera, jest osobną aplikacją lub pochodzi z jakiejś libki. Wtedy weryfikacja interakcji na mocku może mieć sens.

1 użytkowników online, w tym zalogowanych: 0, gości: 1