Okres wakacyjno – urlopowy czas zakończyć i wrócić do regularnego blogowania 🙂
Test jednostkowy powinien trwać milisekundy, a tu mija pierwsza, piąta, dziesiąta sekunda, coś jest nie tak. Zaglądamy do kodu czyżby nasz test jednostkowy był uzależniony od kosztownej zewnętrznej zależności. Pudło, wszystkie rzeczywiste zależności na potrzeby testów jednostkowych zastały zastąpione przez atrapę (mock). W ramach wpisu nie rozróżniam atrap na Fake, Stub, Mock, Dummy, Spy, wszystkie obiekty traktuję jako Mock. W celu zrozumienia problemu przejdźmy do poniższego przykładu.
Analizowany przypadek
Przypadek testowy postanowiłem uprościć do minimum, by uniknąć wchodzenia w bardziej złożoną logikę biznesową. Mamy w klasie Calc metodę wykonującą obliczenia na obszarze 1000×1000 metrów z krokiem co 1 metr, czyli 1000×1000 elementów. W ramach obliczeń dla lokalizacji x, y będzie odczytywana wartość wysokości terenu z zewnętrznego serwisu, która wymagana jest do dalszych obliczeń (w naszym przypadku pomnożymy ją przez stałą wartość). W ramach testów jednostkowych zewnętrzną zależność zostanie zasymulowana makietą.
public interface ICalc { float[] CalcArea(int width, int height, int step); }
public interface IMapService { float GetTerrainHeight(int x, int y); }
public class Calc : ICalc { private readonly IMapService mapService; public Calc(IMapService mapService) { this.mapService = mapService; } public float[] CalcArea(int width, int height, int step) { int ncols = width%step == 0 ? width/step : width/step + 1; int nrows = height%step == 0 ? height/step : height/step + 1; var area = new float[ncols * nrows]; const float factor = 0.6f; for (int i = 0; i < nrows; i++) { int n = i * nrows; for (int j = 0; j < ncols; j++) { area[n + j] = mapService.GetTerrainHeight(i, j) * factor; } } return area; } }
W ramach testów zewnętrzna zależność IMapService została zasymulowana przez atrapę z frameworka Moq, NSubstitute i ręczną implementacją sztucznego obiektu. Czasy wyświetlane przez runnera testów nie są dość dokładne, tym samym do testów wydajności, w celu zmierzenia czasu wykonania została wykorzystana biblioteka BenchmarkDotNet. Część wspólną kodu, która nie wpływała na różnicę czasowe analizowanych trzech przypadków, czyli blok Assert i deklaracja zmiennej z oczekiwanym wynikiem została przeze mnie pominięta (usunięta). Poniżej znajdują się kody trzech metod, które zostały przeanalizowane pod kątem czasu wykonania.
using BenchmarkDotNet.Attributes; using Moq; using NSubstitute; namespace CalcBenchmark { public class CalcAreaCallBenchmark { [Benchmark] public void CalcWithNSubstitute() { var mockMapService = Substitute.For<IMapService>(); mockMapService.GetTerrainHeight(Arg.Any<int>(), Arg.Any<int>()).Returns(4.5f); var calc = new Calc(mockMapService); calc.CalcArea(1000, 1000, 1); } [Benchmark] public void CalcWithMoq() { var mockMapService = new Mock<IMapService>(); mockMapService.Setup(x => x.GetTerrainHeight(It.IsAny<int>(), It.IsAny<int>())).Returns(4.5f); var calc = new Calc(mockMapService.Object); calc.CalcArea(1000, 1000, 1); } [Benchmark] public void CalcWithoutFramework() { var mockMapService = new MockMapService(); var calc = new Calc(mockMapService); calc.CalcArea(1000, 1000, 1); } } }
public class MockMapService : IMapService { public float GetTerrainHeight(int x, int y) { return 4.5f; } }
using BenchmarkDotNet.Running; namespace CalcBenchmark { class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run(typeof(CalcAreaCallBenchmark)); } } }
Wyniki Benchmarku
W analizowanym przypadku wykorzystanie bibliotek do mockowania jest nie korzystne wydajnościowo. Test bez frameworka wykonuje się ok. 2083 razy szybciej niż przy użyciu frameworka NSubstitute. Dla Moq uzyskano trochę lepszy czas, ale i tak w porównaniu do metody CalcWithoutFramework wypada słabo.
Podsumowanie
Co z tym można zrobić? Po pierwsze zajrzeć do kodu i przeanalizować czy logika biznesowa została prawidłowo zaimplementowana. Po drugie warto pamiętać ze istnieje strata wydajności, gdyż te frameworki nie są przeznaczone do pracy z dużymi danymi, oraz dużą ilością wywołań mockowanych metod w ramach testu jednostkowego. Frameworki mają nam pomagać, ale pamiętajmy żeby je stosować z głową.