Testy jednostkowe - mockowanie bez interfejsów

0

Witam, rozpocząłem niedawno pracę jako programista i dostałem zadanie napisać testy jednostkowe. Nie wiedziałem nic wcześniej na ich temat tak więc po tygodniu zapoznania się z aplikacją i poczytaniu o testach przystąpiłem do pisania.

Od ranu napotkałem pewien problem ponieważ w Internecie piszą, że powinno operować się na interfejsach. Pozwólcie, że przedstawie trochę kodu:

// Fragment klasy, która chcę przetestować i ciało metody, od której zaczynam.
public class SomeClass
{
 public SomeClass(ApplicationDbContext db)
:base(db) // po prostu w klasie nadrzędnej jest ApplicationDbContext db;
{
}
// Metoda, którą chcę  przetestować.
    public bool AddOrUpdateSomeProperty(SomeOtherClass doc)
        {
            this.db.SomeProperty.AddOrUpdate(doc); // (1)

            try
            {
                this.db.SaveChanges(); // (2)
                return true;
            }
            catch (Exception)
            {
                //TODO: logging
                return false;
            }
        }
}

Tak więc testując tą klasę i metodę muszę dla pierwszego przypadku gdy metoda się wykona cała(true):

  1. Z mockować ApplicationDbContext ponieważ testy są na lokalniej maszynie i nie ma dostępu do bazy danych i serwera.
  2. (1) muszę zrobić coś z db by ta metoda zwróciła śmieci by test się nie wywalił
  3. (2) tutaj tak samo muszę ustawić coś by test się nie wywalił
    Dla przypadku gdy false:

#(2) musi zwrócić Exception
I właśnie z tym mam problem ponieważ db.SomeProperty to jest DbSet<SomeOtherClass> nie jest to ani interfejs ani składowa wirtualna.

ApplicationDbContext wygląda w fragmencie tak:

 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
  public DbSet<SomeOtherClass> SomeProperty{ get; set; }
}

Próbowałem przy użyciu NSubstitute polecanym na tym forum i na innych lecz w dokumentacji wyczytałem, że używając Substitute na klasach wykonywane są nie wirtualne składowe tak więc wykonują się, nie które metody, które wywalają test ponieważ odwołują się do składników bazy danych i serwera. Więc ten framework chyba odpada. A może Substitute.ForPartOf?

Drugi wziąłem pod młotek Moq i tutaj próbuje stworzyć mock ApplicationDbContext przechodzi ładnie lecz nie mogę sobie poradzić z tym DbSetem oraz SaveChanges ponieważ w kontekście jest SaveChanges() i SaveChanges(string x) i działa tylko dla drugiej wersji a dla pierwszej wywala błąd.

Dla takie testu:

[Fact]
        public void AddOrUpdateSomeProperty_Success()
        {
            // Moq
           // arrange
            var contextMock = new Mock<ApplicationDbContext>();

            var provider = new SomeClassProvider(contextMock.Object);

            // act
            bool result = provider.AddOrUpdateSomeProperty(new SomeClass());

            // assert
            Assert.True(result);
        }

Wyrzuca wyjątek w punkcie (1) testowanej metody:

An exception of type 'System.ArgumentNullException' occurred in EntityFramework.dll but was not handled in user code
Additional information: Wartość nie może być zerowa.

Po dodaniu:

contextMock.Setup(x => x.ExpensesDocuments.AddOrUpdate(It.IsAny<ExpensesDocument>()));

Wyrzuca w punkcie (1) wyjątek:

An exception of type 'System.NotSupportedException' occurred in Moq.dll but was not handled in user code

Additional information: Expression references a method that does not belong to the mocked object: x => x.ExpensesDocuments.AddOrUpdate<ExpensesDocument>(new[] { It.IsAny<ExpensesDocument>() })

Ktoś może pomóc i powiedzieć co jest nie tak i jak się za to zabrać? Dodam jeszcze, że nie ma możliwości edycji ApplicationDbContext i dodanie interfejsu. Aplikacja w .NET 4.5

0

I właśnie z tym mam problem ponieważ db.SomeProperty to jest DbSet<someotherclass> nie jest to ani interfejs ani składowa wirtualna.

W czym problem by dodać modyfikator virtual?

W przypadku EF od wersji 6 w górę, można robić to tak https://msdn.microsoft.com/en-us/data/dn314429.aspx#nonQuery .

W przypadku problemów, możemy też opakować daną metodę w dodatkową metodę wirtualną, np w DbContext'cie:

public class SampleContext : DbContext, ISampleContext
    {
        public virtual DbSet<Sample> Samples { get; set; }

        //public DbSet<SampleItem> SampleItems { get; set; }
        public virtual DbSet<SampleItem> SampleItems { get; set; }

        public SampleContext() : base("DefaultConnection")
        {
            modelBuilder.Configurations.Add(new SampleItemMap());
            base.OnModelCreating(modelBuilder);
        }

        public virtual void SetModified(object entity)
        {
            Entry(entity).State = EntityState.Modified;
        }

        public virtual void SetDeleted(object entity)
        {
            Entry(entity).State = EntityState.Deleted;
        }
    }

    interface ISampleContext
    {
        void SetModified(object entity);
        void SetDeleted(object entity);
    }

        [Fact]
        public void EditSampleTest()
        {
            var mockSet = new Mock<DbSet<Sample>>();

            var mockContext = new Mock<SampleContext>();
            mockContext.Setup(x => x.Samples).Returns(mockSet.Object);
            mockContext.Setup(x => x.SetModified(It.IsAny<object>()));

            EditSampleCommandHandler commandHandler = new EditSampleCommandHandler(mockContext.Object);
            commandHandler.Execute(new EditSampleCommand(new Sample() { SampleId = 1, Name = "test" }));

            mockSet.Verify(x => x.Attach(It.IsAny<Sample>()), Times.Once);
            mockContext.Verify(x => x.SetModified(It.IsAny<Sample>()), Times.Once);
            mockContext.Verify(x => x.SaveChanges(), Times.Once);
        }
 

Jeżeli nie można zupełnie użyć interfejsów ani modyfikatora virtual, to robienie testów, mocków będzie raczej wyjątkowo trudne.

0

No rzecz w tym, że nie mogę zmienić ciała ApplicationDbContext.

0
Pijany Szczur napisał(a):

No rzecz w tym, że nie mogę zmienić ciała ApplicationDbContext.

A możesz je opakować? Jeśli tak, to opakuj w jakąś klasę DAL z interfejsem i do SomeClass przekazuj ApplicationDbContext.

No i pytanie podstawowe, po co w ogóle chcesz to testować jednostkowo?

0

Opakować? Jak? Po co?

Mam dostęp do ApplicationDbContext mam wgląd w tą klasę ponieważ jest ona zawarta w projekcie tylko nie pozwalają mi nic zmieniać.

Po co testy jednostkowe? To moje zdanie. Nie mają żadnych testów.
Chyba każdy system powinien mieć testy co nie?

0
Pijany Szczur napisał(a):

Opakować? Jak? Po co?

Żeby móc mockować i testować. Goły context EF jest nietestowalny. Dlatego mimo, że sam w sobie jest generycznym repozytorium i kontenerem unit of work, wszyscy i tak opakowują go w generyczne repozytorium i kontener UoW. Ot, takie uroki używania ORMów zaprojektowanych przez praktykantów Microsoftu.

Po co testy jednostkowe? To moje zdanie. Nie mają żadnych testów.
Chyba każdy system powinien mieć testy co nie?

Nie. Każdy system powinien mieć sensowne testy.

Jednostkowo testuje się logikę aplikacji, a nie operacje na bazie. Test zapisu do bazy to nie jest test jednostkowy lecz integracyjny i powinien z bazy korzystać.

0

Czyli rozumiem, że powinienem opakować ApplicationDbContext korzystając z UoW?

Ale gdy wszystkie klasy mają wstrzykiwany ApplicationDbContext przez konstruktor.

0

No cóż, ktoś to źle zaprojektował. Spytaj autora tego pomysłu, jak do tego napisać testy, skoro tak to wymyślił.

Ja nie widzę innego sposobu niż opakowanie albo pisanie testów integracyjnych.

0

Ok, dzięki wielkie.

Skontaktuje się z przełożonym i powiem.

Czyli jedynym sensownym rozwiązaniem by pisać normalnie testy jednostkowe jest przebudowanie aplikacji i stworzenie kontekstu dla ApplicationDbContext i zastosowanie UoW?
Czy sama podmiana na interfejs załatwi sprawę?

0

Wysłałem zapytanie do przełożonego. A między czasie staram się przetestować inne metody np:

var invoice = this.db.InvoiceDocuments.FirstOrDefault(x => x.DocumentNo == docId); // Tutaj wywala błąd, że wartość nie może być zerem. 
// poniżej sprawdzenie czy invoice nie jest nullem. 

Zrobiłem coś takiego:

  // assert
            var mockSet = new Mock<DbSet<InvoiceDocument>>();
            var mockContext = new Mock<ApplicationDbContext>();
            mockContext.Setup(m => m.InvoiceDocuments).Returns(mockSet.Object);

            var provider = new ExpensesProvider(mockContext.Object);

            // act
            bool result = provider.AddCashReportPayment("1", TransactionType.Payin,
             PaymentType.Cash, 1);

            // assert
            Assert.True(result);

I pojawia się problem ponieważ FirstOrDefalt jest metodą rozszerzenia(statyczna) i jest metodą obiektu DbSeta nie można jej zmockować. Co z takim fantem zrobić?
W necie pojawią się informacje o Microsoft Fakes ktoś korzystał i przyzna, że to do takich spraw?

0
Pijany Szczur napisał(a):

I pojawia się problem ponieważ FirstOrDefalt jest metodą rozszerzenia(statyczna) i jest metodą obiektu DbSeta nie można jej zmockować. Co z takim fantem zrobić?

Nie mockować.
Nie testuj kodu frameworka, testuj kod projektu.

0

No rozumiem, że testować mam kod metody lecz jeśli nie zamockuje to test wywali się w momencie odwołania do kontekstu aplikacji.

Chyba czegoś dokładnie nie zrozumiałem.
Mógłbyś wyjaśnić dogłębniej?

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