Testy jednostkowe są jednym z fundamentów współczesnego procesu tworzenia oprogramowania. Dzięki nim możemy upewnić się, że nasz kod działa zgodnie z oczekiwaniami, a refaktoryzacja lub nowe funkcjonalności nie wprowadzają regresji. Jednak nawet najlepsi programiści popełniają błędy, które mogą sprawić, że testy przestają spełniać swoją rolę.
W tym artykule omówię 9 najczęstszych błędów w testach jednostkowych w C#, które popełnia większość programistów i przede wszystkim pokaże, jak ich unikać. Dzięki praktycznym przykładom dowiesz się, jak pisać skuteczne, czytelne i niezawodne testy.
Co to są testy jednostkowe?
Testy jednostkowe to krótkie fragmenty kodu, które w izolacji weryfikują działanie małych jednostek oprogramowania, takich jak funkcje, klasy lub metody. Aby testy były skuteczne, powinny spełniać kilka kluczowych zasad:
- Izolacja: Każdy test działa niezależnie od innych.
- Przewidywalność: Wyniki testów nie zależą od środowiska ani zmiennych zewnętrznych.
- Szybkość: Testy jednostkowe powinny być wykonywane w ułamkach sekundy.
- Łatwość utrzymania: Kod testów powinien być równie czytelny i dobrze zorganizowany, jak kod aplikacji.
1. Brak izolacji testów
Problem
Testy jednostkowe muszą być niezależne od siebie. Jeśli jeden test zmienia globalny stan, może to wpłynąć na wynik innego testu, co prowadzi do trudnych do znalezienia błędów.
Kod problematyczny:
public static class Counter
{
public static int Value { get; private set; }
public static void Increment() => Value++;
}
public class CounterTests
{
[Test]
public void Increment_ShouldIncreaseValue()
{
Counter.Increment();
Assert.AreEqual(1, Counter.Value);
}
[Test]
public void MultipleIncrements_ShouldWorkCorrectly()
{
Counter.Increment();
Counter.Increment();
Assert.AreEqual(2, Counter.Value); // Zawiedzie, jeśli testy uruchomisz razem
}
}
RozwiązanieZresetuj stan przed każdym testem za pomocą SetUp:
public class CounterTests
{
[SetUp]
public void SetUp()
{
Counter.Value = 0; /* Reset stanu */
}
[Test]
public void Increment_ShouldIncreaseValue()
{
Counter.Increment();
Assert.AreEqual(1, Counter.Value);
}
}
2. Testy zależne od zewnętrznych zasobów
Problem
Testy jednostkowe powinny być szybkie, niezależne i izolowane. Jeśli testy korzystają bezpośrednio z zewnętrznych zasobów, takich jak baza danych, API lub system plików, przestają być testami jednostkowymi. Stają się wolne, podatne na błędy środowiskowe (np. niedostępność bazy danych) i trudniejsze do utrzymania.
Kod problematyczny:
public class UserService
{
public string GetUserNameFromDatabase()
{
/* Symulacja pobierania danych z bazy */
throw new NotImplementedException();
}
}
public class UserServiceTests
{
[Test]
public void GetUserNameFromDatabase_ShouldReturnUserName()
{
var service = new UserService();
var result = service.GetUserNameFromDatabase();
Assert.AreEqual("Jan Kowalski", result); // Test zawiedzie bez prawdziwej bazy danych
}
}
Rozwiązanie
Zamiast rzeczywistych zasobów użyj mocków:
public interface IUserRepository
{
string GetUserName();
}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
public string GetUserName() => _repository.GetUserName()?.ToUpper(); /* Przykładowa logika */
}
public class UserServiceTests
{
[Test]
public void GetUserName_ShouldReturnUpperCaseName_WhenUserNameIsValid()
{
/* Arrange */
var mockRepository = new Mock<IUserRepository>();
mockRepository.Setup(repo => repo.GetUserName()).Returns("Jan Kowalski");
var service = new UserService(mockRepository.Object);
/* Act */
var result = service.GetUserName();
/* Assert */
Assert.AreEqual("JAN KOWALSKI", result);
}
}
3. Brak testów dla przypadków brzegowych
Problem
Testy często obejmują tylko poprawne ścieżki działania kodu, ignorując błędy i przypadki brzegowe.
Kod problematyczny:
public int Divide(int a, int b) => a / b;
[Test]
public void Divide_ShouldReturnCorrectQuotient()
{
Assert.AreEqual(5, Divide(10, 2));
}
Rozwiązanie
Zawsze testuj zarówno przypadki sukcesu, jak i błędy:
public class MathTests
{
[Test]
public void Divide_ShouldThrowDivideByZeroException_WhenDividingByZero()
{
Assert.Throws<DivideByZeroException>(() => Divide(10, 0));
}
[Test]
public void Divide_ShouldReturnCorrectResult_WhenInputsAreValid()
{
Assert.AreEqual(5, Divide(10, 2));
}
}
4. Nieczytelne nazwy testów
Problem
Nazwy takie jak Test1 czy CheckSomething nie informują o tym, co dokładnie testuje dana metoda.
Rozwiązanie
Używaj opisowych nazw, które jasno określają, co jest testowane:
[Test]
public void Add_TwoPositiveNumbers_ShouldReturnTheirSum()
{
Assert.AreEqual(5, Add(2, 3));
}
5. Zależność od zmiennych czynników
Problem
Kod zależny od DateTime.Now, Random lub innych zmiennych czynników prowadzi do niestabilnych wyników testów.
Kod problematyczny:
public bool IsOfficeOpen()
{
return DateTime.Now.Hour >= 9 && DateTime.Now.Hour < 17;
}
Rozwiązanie
Wprowadź interfejs umożliwiający mockowanie:
public interface IClock
{
DateTime Now { get; }
}
public class OfficeService
{
private readonly IClock _clock;
public OfficeService(IClock clock)
{
_clock = clock;
}
public bool IsOfficeOpen()
{
var hour = _clock.Now.Hour;
return hour >= 9 && hour < 17;
}
}
public class OfficeServiceTests
{
[Test]
public void IsOfficeOpen_ShouldReturnTrue_DuringOfficeHours()
{
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2023, 1, 1, 10, 0, 0));
var service = new OfficeService(mockClock.Object);
Assert.IsTrue(service.IsOfficeOpen());
}
}
6. Nadmierne testowanie szczegółów implementacji
Problem
Testy często odwołują się do wewnętrznych szczegółów klasy zamiast do jej zachowania.
Rozwiązanie
Testuj zachowanie, a nie implementację:
[Test]
public void IsUserAdult_ShouldReturnTrue_WhenAgeIs18OrMore()
{
var user = new User { Age = 20 };
Assert.IsTrue(user.IsAdult());
}
7. Pisanie dublujących się testów
Problem
Pisanie osobnych testów dla każdej wartości jest czasochłonne i nieefektywne.
Rozwiązanie
Użyj testów parametryzowanych w NUnit:
public class CalculatorTests
{
[TestCase(2, 3, 6)]
[TestCase(-2, 3, -6)]
[TestCase(0, 5, 0)]
public void Multiply_ShouldReturnCorrectResult(int a, int b, int expected)
{
Assert.AreEqual(expected, Multiply(a, b));
}
}
8. Zbyt ogólnikowe asercje
Problem
Asercje, które sprawdzają tylko część danych, mogą być nieprecyzyjne.
Rozwiązanie
Korzystaj z dokładnych asercji:
[Test]
public void GetUser_ShouldReturnUserWithCorrectNameAndAge()
{
var user = GetUser();
Assert.Multiple(() =>
{
Assert.AreEqual("Jan", user.Name);
Assert.AreEqual(30, user.Age);
});
}
9. Ignorowanie wydajności testów
Problem
Zbyt wolne testy mogą zniechęcać do ich regularnego uruchamiania.
Rozwiązanie
Optymalizuj testy, używaj mocków zamiast rzeczywistych zasobów i unikaj nadmiernego obciążenia.
Chcesz pisać profesjonalne testy jednostkowe?
Jeśli chcesz pogłębić swoją wiedzę o testach jednostkowych, unikać błędów i dowiedzieć się, jak efektywnie testować aplikacje w C#, zapraszam Cię do mojego szkolenia online:
➡️ szkolatestowjednostkowych.pl
W szkoleniu dowiesz się m.in.:
- Jak pisać czytelne i łatwe w utrzymaniu testy.
- Jak testować metody zależne od zewnętrznych zasobów, takich jak bazy danych czy API.
- Jak korzystać z narzędzi takich jak Moq, FluentAssertions czy AutoFixture.
- Jak automatyzować testy jednostkowe w procesie CI/CD.
To idealne szkolenie zarówno dla początkujących, jak i dla tych, którzy chcą uporządkować swoją wiedzę. Dołącz już dziś i podnieś swoje umiejętności na wyższy poziom!
➡️ Szkoła Testów Jednostkowych
Podsumowanie
Testy jednostkowe są kluczowym narzędziem każdego programisty, ale ich skuteczność zależy od jakości implementacji. Unikając opisanych błędów, możesz pisać testy, które są niezawodne, łatwe w utrzymaniu i czytelne. Dzięki temu Twoje projekty będą bardziej stabilne i łatwiejsze w rozwoju.
Pamiętaj: dobre testy to nie tylko inwestycja w jakość kodu, ale również w komfort pracy Twojego zespołu!