Nietestowanie prywatnych metod - całkowicie się z tym nie zgadzam. Testy jednostkowe powinny testować każdą metodę, sprawdzać czy dla prawidłowych danych zwraca prawidłowe wyniki i czy dla danych nieprawidłowych zachowuje się przewidywalnie (rzuca określone wyjątki/zwraca określoną wartość). Jeśli nie będziesz testować prywatnych metod (a w praktyce internal), to nie będziesz w stanie napisać testów jednostkowych, tylko testy integracyjne.
Podwójne znaki podkreślenia w nazwach - wygląda to paskudnie. Jeśli dwa są lepsze niż jeden, to czemu nie trzy albo cztery? Wygląda jak powrót do notacji węgierskiej.
U nas używamy nieco innej składni - jeśli testowane są przypadki typowego, poprawnego działania, to metoda testująca nazywa się tak samo jak metoda testowana. Jeśli testujemy zachowanie dla nieprawidłowych danych, to nazwę stanowi nazwa testowanej metody + podkreślenie + nazwa case'a, np. SendPasswordRetrievalEmailToUser_UserNotFound. Jak widać trochę podobnie do tego, co zaproponował somekind, ale bez podwójnych podkreśleń (fuu!) i bez zawierania w nazwie tego, co ma się zadziać. To ostatnie jest spowodowane DRY - jeśli ktoś robiąc zmianę w testowanej metodzie, a potem w teście, zapomni zmienić nazwę testu, to potem będą głupoty - nazwa testu będzie sugerować jedno, jego działo będzie robić drugie. A jak wiadomo, jeśli coś złego może się przydarzyć, to na pewno się przydarzy ;-)
BTW "KeyNotExists" to nie po angielsku.
Nietestowanie kontrolerów to podobny żart jak nietestowanie prywatnych metod. Przecież to nadal kod pisany przez programistę i potem potencjalnie modyfikowany przez kolejnych programistów. Z tego względu MUSI być przykryty testem, chociażby tylko po to, żeby sprawdzić że mock został wywołany i jego wynik został wepchnięty do modelu.
Patrząc na kod testów @AlfaLeporis:
Jeśli używałbyś NUnita, to warto zapoznać się z atrybutami SetUp, TearDown, TestFixtureSetUp i TestFixtureTearDown. Potem zamiast w każdym teście wywoływać var service = GetServiceWithData();
(DRY!) robisz sobie pole private Service sut; (sut od Service Under Tests) i w Setup inicjalizujesz sobie je na nowo przed każdym testem, tak samo robisz z mockami. Z kolei w Teardown walidujesz wszystkie mocki.
U nas wygląda to tak (część kodu wycięta) - NUnit + Moq:
public class UserControllerTests
{
private Mock<IUserService> userServiceMock;
private Mock<ILicenceContextService> licenceContextServiceMock;
...
private Mock<UserController> cutPartialMock;
private UserController cut; // cut, bo Controller Under Tests
#region Setup/Teardown
[SetUp]
public void TestSetUp()
{
userServiceMock = new Mock<IUserService>();
licenceContextServiceMock = new Mock<ILicenceContextService>(MockBehavior.Strict);
...
cut = new UserController(userServiceMock.Object, licenceContextServiceMock.Object, ...);
cutPartialMock = new Mock<UserController>(MockBehavior.Loose, userServiceMock.Object, licenceContextServiceMock.Object, ...) { CallBase = true };
}
[TearDown]
public void TestTearDown()
{
cutPartialMock.VerifyAll();
userServiceMock.VerifyAll();
licenceContextServiceMock.VerifyAll();
...
}
#endregion
// i jakiś przykładowy test, od razu na partial mocku żeby było więcej wiedzy
[TestCase(true, true, null)]
[TestCase(false, true, ErrorMessages.NoSuchUser)]
[TestCase(true, false, ErrorMessages.CantCancelPendingAccountDelete)]
public void CancelDeleteUserAccount(bool userExist, bool isDeleteCanceled, string expectedMessage)
{
var user = new User { Id = 128123, Email = "[email protected]", DeleteProcedureHash = "abc" };
userServiceMock.Setup(x => x.GetUserById(user.Id)).Returns(userExist ? user : null);
if (userExist)
userAccountServiceMock.Setup(x => x.CancelDelete(user.Email, user.DeleteProcedureHash)).Returns(isDeleteCanceled);
var result = cut.CancelDeleteUserAccount((int)user.Id);
var data = (JsonStatusModel)result.Data;
Assert.AreEqual(expectedMessage._(), data.statusMessage);
}
Jeszcze jedna sprawa - ze względu na bazę danych niektóre testy muszą być integracyjne z dwóch powodów. Po pierwsze EF nie pozwala na mockowanie, więc na najniższym poziomie - tam gdzie testuje się metody pracujące z bazą - trzeba pracować z "żywą" bazą danych. Po drugie ze względu na indeksy i klucze obce (albo i triggery), a także ze względu na współbieżność i transakcje czasem może się okazać, że jednostkowo/na mockach cały proces działa fajnie, a mimo to w praktyce się wywala. Z tego powodu czasem warto dodać czasem też test integracyjny sprawdzający cały proces (acz testowanie zachowania transakcji - UpdateException, OptimisticConcurrencyException - to rzecz koszmarnie trudna do automatycznego odtworzenia).