Moq: (asynchorniczna) testowana metoda zwraca null

0

Witam,
jak w temacie staram się napisać metodę testową dla metody, która jest w asynchroniczny sposób wyciąga dane z bazy.
Sama metoda wygląda w uproszczeniu tak:

        public async Task<MyViewModel> GetData(long id)
        {
            try
            {
                //_database to instancja DbContext wstrzykiwana do konstruktora serwisu
                var query = await (from invoice in _database.Invoice
                             //tutaj kilka joinów
                             where invoice.id == id
                             select new { /* tutaj selecty właściwych obiektów */ }).ToListAsync();

                var model = query.Select(r => new MyViewModel()
                {
                    //tutaj mapuję na MyViewModel, którego pola tu struktury
                }).FirstOrDefault();

                return model;
            }
            catch (Exception ex)
            {
                log.Error(ex.Message);
                return null;
            }
        }

Ok, metoda działa. Czas na napisanie testu dla niej.

[TestClass]
    public class MyServiceTest
    {
        private IMyService _service;
        private Mock<ApplicationDbContext> _db;

        [TestInitialize]
        public void Initialize()
        {
            _db = new Mock<ApplicationDbContext>();
        }

        [TestMethod]
        public async Task GeData_ShouldReturnElement()
        {
            //ARRANGE
            var invoices = Builder<invoice>.CreateListOfSize(3).Build();
            var invoicesStates = Builder<invoice_state>.CreateListOfSize(3).Build();
            //i tak dalej dla każdej tabeli użytej w joinie

            _db.Setup(x => x.Invoice).Returns(this.SetupMock<invoice>(invoices).Object);
            _db.Setup(y => y.InvoiceState).Returns(this.SetupMock<invoice_state>(invoicesStates).Object);    
            //i tak dalej dla każdej tabeli użytej w joinie    

            _service = new MyService(_db.Object);

            //ACT
            var result = await _service.GetData(2);

            //ASSERT
            Assert.IsNotNull(result);  
            Assert.IsInstanceOfType(result, typeof(MyViewModel)); 
            Assert.AreEqual(result.Id, 2);
        }

        private Mock<DbSet<T>> SetupMock<T>(IList<T> entities) where T : class
        {
            var mockSet = new Mock<DbSet<T>>();
            mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(entities.AsQueryable().Provider);
            mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(entities.AsQueryable().Expression);
            mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(entities.AsQueryable().ElementType);
            mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(entities.GetEnumerator());
            mockSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>((s) => entities.Add(s));
            return mockSet;
        }
}

result jest zawsze nullem. Jeżeli testowaną metodę zrobię synchroniczną to test przechodzi.
Gdzie leży problem?

1

Jest nullem, bo rzucany jest wyjątek? Jeśli tak to jaki?
Tak w ogóle, to jaki jest cel tego testu? Bo widzę tu testowanie głównie Moq i LINQ, ale one już są dobrze przetestowane i nie trzeba bardziej.

A tak poza tym, to wygląda na to, że masz jakąś dziwną strukturę bazy, z której niewydajnie pobierasz dane.

0
somekind napisał(a):

Jest nullem, bo rzucany jest wyjątek? Jeśli tak to jaki?

Kurczę, masz rację:

Test method ProjUnitTests.Services.MyServiceTest.GeData_ShouldReturnElement threw exception:
System.InvalidOperationException: The source IQueryable doesn't implement IDbAsyncEnumerable<<>f__AnonymousType5`2[Invoices.Models.Tables.invoice, (..) <- tutaj reszta tabel z joinów ]>. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.

...

somekind napisał(a):

Tak w ogóle, to jaki jest cel tego testu? Bo widzę tu testowanie głównie Moq i LINQ, ale one już są dobrze przetestowane i nie trzeba bardziej.

A to jest w sumie początek dłuższej rozmowy, bo przecież znaczna część aplikacji webowych to typowe CRUDy - pobierz, usuń, zaktualizuj, co sprowadza się głównie do działań na bazie i linq. W ten sposób można by chyba wykluczyć naprawdę sporo projektów z testowania.
Cel tego testu to upewnienie się, że otrzymam właściwy obiekt. Zmiana na bazie lub w klasie ViewModelu wysypie test.

somekind napisał(a):

A tak poza tym, to wygląda na to, że masz jakąś dziwną strukturę bazy, z której niewydajnie pobierasz dane.

Zgadzam się, ale to zaszłość i nie mogę z tym nic zrobić, a osoba która może - zwyczajnie nie chce :(

1
boogi napisał(a):

No właśnie problem w tym, że nie ma wyjątku. Test przechodzi i wyrzuca komunikat: Result Message: Assert.IsNotNull failed.

Ale wiesz to na podstawie wyniku testu, czy debugowałeś wykonywany kod?
Co masz w liście query? Co w model w linijce z return?

A to jest w sumie początek dłuższej rozmowy, bo przecież znaczna część aplikacji webowych to typowe CRUDy - pobierz, usuń, zaktualizuj, co sprowadza się głównie do działań na bazie i linq. W ten sposób można by chyba wykluczyć naprawdę sporo projektów z testowania.

A i owszem. Testować jednostkowo należy logikę i obliczenia, a nie CRUD.

Cel tego testu to upewnienie się, że otrzymam właściwy obiekt. Zmiana na bazie lub w klasie ViewModelu wysypie test.

No właśnie nie. Można usunąć bazę, a wszystkie testy przejdą, bo wszystko jest zamockowane.

Moim zdaniem takie testy są zupełnie bezwartościowe, jeśli chcesz testować czy dane są prawidłowo pobierane z bazy, pobieraj je z bazy, a nie z mocka.

A co do edycji posta, to wygląda na to, że mockujesz synchroniczne metody EF zamiast asynchronicznych. Dokładniej nie pomogę, bo ani z EF nie korzystam, ani nie mam doświadczenia z mockowaniem czegoś, czego mockować się nie powinno.
Poza tym chyba wszystko jest w dokumentacji: https://msdn.microsoft.com/en-us/data/dn314429

0
somekind napisał(a):

No właśnie nie. Można usunąć bazę, a wszystkie testy przejdą, bo wszystko jest zamockowane.

Ale zmiana na bazie (zmiana typu, nazwy, powiązania jako klucz obcy, obostrzeń kolumny itp.) wymusi zmianę w klasach encji, które mapują tabele z db. I teraz jeśli te zmiany wpłyną negatywnie na testowane funkcje to testy to wyłapią bez względu na to, że baza jest zamockowana.

Edit
Znalazłem, do asynchronicznych działań potrzeba innego sposobu mockowania. Całość jest opisana tutaj: https://msdn.microsoft.com/en-us/data/dn314429#async
W skrócie dodałem 3 klasy z powyższego linka (DbAsyncEnumerable, DbAsyncEnumerator, DbAsyncQueryProvider) i zmodyfikowałem swoją metodę SetupMock na:

private Mock<DbSet<T>> SetupMockAsync<T>(IList<T> entities) where T : class
{
     var mockSet = new Mock<DbSet<T>>();
     mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new DbAsyncQueryProvider<T>(entities.AsQueryable().Provider));
     mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(entities.AsQueryable().Expression);
     mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(entities.AsQueryable().ElementType);
     mockSet.As<IDbAsyncEnumerable<T>>().Setup(m => m.GetAsyncEnumerator()).Returns(new DbAsyncEnumerator<T>(entities.GetEnumerator()));
     mockSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>((s) => entities.Add(s));
     return mockSet;
}

Dzięki @somekind za naprowadzenie poświęcony czas :)

1
boogi napisał(a):

Ale zmiana na bazie (zmiana typu, nazwy, powiązania jako klucz obcy, obostrzeń kolumny itp.) wymusi zmianę w klasach encji, które mapują tabele z db. I teraz jeśli te zmiany wpłyną negatywnie na testowane funkcje to testy to wyłapią bez względu na to, że baza jest zamockowana.

W sytuacji, gdy model EF jest aktualny, a Ty mapowanie na ViewModele piszesz i tak ręcznie, to takie usunięcie tabeli, kolumny czy zmianę typu danych wyłapie Ci kompilator. Gdybyś miał testy integracyjne, to wyłapałyby np. sytuację, w której zmieniłeś bazę, a nie zmieniłeś modelu EF i to by jeszcze mogło mieć jakiś sens.
A tak, to męczysz się nadaremno, te testy nigdy niczego nie wykryją.

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