Sõltuvuste katkestamine
Sõltuvuse vaste testimise maailmas on dependency. Sõltuvuseks loeme me kõiki klasse ja väliseid teenuseid, mida testitav meetod kasutab. Nii võib ASP.NET Core kontrolleri sõltuvusteks lugeda konstruktori kaudu kaasa antud teenuste klassid ja DbContext-i.
näide mõne kontrolleri baasil
Ühiktestides soovime me eeltoodud sõltuvustest vabaneda. Milleks? Põhjused on lihtsad:
-
Iga väline sõltuvus tähendab täiendavat ajakulu - meenutage, et ühiktestid peavad jooksma võimalikult kiiresti, sest muidu jäävad need unarusse ning meile moodustub testidest üks surnud koodibaas, mis ei ole rakenduse koodiga enam sünkroonis.
-
Iga ühiktest peab testima ainult ühte funktsionaalsust - kui me kasutame testides nö. väliseid klasse, mis päriselt midagi teevad, siis sõltuvad meie testid ka nendes klassides toimuvast. See läheb jällegi vastuollu ideega, et iga ühiktest testib ainult konkreetse meetodi või omaduse loogikat.
Võltsklassid
Peamised vahendid sõltuvuste katkestamiseks on võltsklassid (ametlik termin fakes), mille ülesandeks on testimisel tagastada soovitud väärtusi. Mingit loogikat need klassid ei kanna. Võltsklassid jagunevad kaheks:
- Stub - testiprojektis loodud võltsklass, mida saame kasutada testitava koodibaasi vastava klassi asemel (näiteks UserServiceStub, mis kasutab sarnaselt UserService klassile IUserService interface-i).
- Mock - testide jooksutamise käigus dünaamiliselt genereeritud võltsklasss, kus seadistame meetodid ja nende tagastatavad väärtused, mida meil testi jooksutamisel vaja on. Mockide juurde tuleme tagasi edaspidi.
Vaatleme näiteks klassi UserService
, kuhu antakse kaasa IUserRepository
. Rakenduse jooksutamisel viib ASP.NET Core ise IUserRepository
ja UserRepository
dependnecy injectioni abil kokku ning konstrueerib korrektselt UserRepository
tüüpi objekti.
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
publivc UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public User GetById(int id)
{
return _userRepository.GetById(id);
}
// Ülejäänud kood
}
UserService
klassi testimisel soovime me vältida UserRepository
kasutamist ning võtame selle asemel kasutusele testprojektis defineeritud võltsklassi UserRepositoryStub
.
public UserRepositoryStub : IUserRepository
{
public readonly IList<User> Users { get; private set; }
public UserRepositoryStub()
{
Users = new List<User>();
}
public User GetById(int id)
{
return Users.FirstOrDefault(u => u.Id == id);
}
// Ülejäänud IUserRepository meetodid
}
UserService
testides saame me edaspidi kasutada UserRepositoryStub
klassi, mida saame testides vastavalt vajadusele seadistada. UserRepositoryStub
klassil on avalik omadus Users
(kasutajate loend, millega võtlsklass opereerib) ja selle kaudu saame anda repositoryle andmeid, mida UserService
testimisel kasutab.
public class UserServiceTests
{
[Fact]
public void GetById_return_null_if_user_is_not_found()
{
// Arrange
var repository = new UserRepositoryStub();
var service = new UserService(repository);
var nonExistentId = -1;
// Act
var user = service.GetById(nonExistentId);
// Assert
Asset.IsNull(user);
}
// Ülejäänud testid
}
Meetodi asendamine
Alati ei ole meil võimalik luua võltsklassi sel teel, et implementeerime vastava interface-i. On täiesti võimalik, et peame laiendama olemasolevat klassi ning kirjutama oma versiooni virtuaalsetest meetoditest, mida me testis kasutame.
public class ClassFromExternalPackage
{
// Definitsioonid
}
Seda klassi me naljalt asendada ei saa, sest puudub interface, mida kasutada. Samas me näeme, et sellel klassil on virtuaalseid meetode, mille kaudu me saaksime testis ette anda sobivaid väärtusi.
Klassi asendamine
Kõige keerukamad on sõltuvused, millest eeltoodud võtetega lahti ei saa. Probleemseks sõltuvuseks võib olla mõni klass, mille meetode me asendada ei saa või klass, mille küljest kutsutakse staatilisi meetode.
Äärmusliku juhtumine vaatleme staatiliste meetoditega klassi, mille abil kontrollitakse mõnda välist seadet. On selge, et ühikteste me selliselt jooksutada ei saa, et mõni päris seade või tooteliin saab meie arvutist päris käske. Olukorra teeb keerukamaks see, et antud klassi lähtekoodi me muuta ei saa - see tuleb seadme tootja poolt. Klassi kohta teame, et sellel on kaks staatilist meetodi BladeUp() ja BladeDown();
public static class LineController
{
static void BladeUp() { /* ??? */ }
static void BladeDown() { /* ??? */ }
}
Testimist vajavaks klassiks olgu LineService, mis kontrollib seadet meie enda kirjutatud loogika järgi.
public class LineService : ILineService
{
private readonly LineController _lineController;
private static object _locker = new object();
public LineService(LineController lineController)
{
_lineController = lineController;
}
public void Cut()
{
lock(_locker)
{
_lineContoller.BladeUp();
Thread.Sleep(5000);
_lineController.BladeDown();
_lineController.BladeUp();
}
}
// Ülejäänud meetodid
}
LineController on probleem, mille peame lahendama - mingil moel peame me olema võimelised kasutama mõnda analoogset klassi, mis päris seadmega ei suhtle. Appi tuleb selline disainimuster nagu Adapter (kasutatakse ka nimetust Wrapper).
Joonis adapter/wrapper kohta
Me loome interface-i ja kaks klassi - üks klass on adapter ümber LineControlleri ja teine on võltsklass LineControlleri asendamiseks.
public interface ILineControllerAdapter
{
void BladeUp();
void BladeDown();
}
public class LineControllerAdapter : ILineControllerAdapter
{
private readonly LineController _lineController;
private static object _locker = new object();
public LineControllerAdapter(LineController lineController)
{
_lineController = lineController;
}
public void BladeUp()
{
_lineController.BladeUp();
}
public void BladeDown()
{
_lineController.BladeDown();
}
}
public class LineControllerStub : ILineControllerAdapter
{
public void BladeUp { }
public void BladeDown { }
}
Nüüd saame muuta LineService klassi ja võtta kasutusele ILineControllerAdapter-i, et saaksime ühiktestimisel kasutada LineController asemel sellele vastavat võltsklassi LineControllerStub.
public class LineService : ILineService
{
private readonly ILineControllerAdapter _lineControllerAdapter;
private static object _locker = new object();
public LineService(ILineControllerAdapter lineControllerAdapter)
{
_lineControllerAdapter = lineControllerAdapter;
}
public void Cut()
{
lock(_locker)
{
_lineControllerAdapter.BladeUp();
Thread.Sleep(5000);
_lineControllerAdapter.BladeDown();
_lineControllerAdapter.BladeUp();
}
}
// Ülejäänud meetodid
}
LineController on nüüd meie kontrolli all ja nüüd saame ohutult kirjutada teste LineService klassi jaoks.
public void Cut_should_run_cut_workflow()
{
var adapter = new LineControllerStub();
var service = new LineService(adapter);
service.Cut();
}