How to dynamically choose a DbContext for API endpoint method - c#

I developed and API that uses a helper class to get the database context for each endpoint function. Now I'm trying to write unit tests for each endpoint and I want to use an In-memory db in my unit test project.
The issue I'm running into is that in order to call the API functions I had to add a constructor to my API controller class. This would allow me to pass the dbContext of the in-memory db to the controller function for it to use. However, since the adding of the constuctor I got the following error when attempting to hit the endpoint:
"exceptionMessage": "Unable to resolve service for type 'AppointmentAPI.Appt_Models.ApptSystemContext' while attempting to activate 'AppointmentAPI.Controllers.apptController'."
UPDATE
controller.cs
public class apptController : Controller
{
private readonly ApptSystemContext _context;
public apptController(ApptSystemContext dbContext)
{
_context = dbContext;
}
#region assingAppt
/*
* assignAppt()
*
* Assigns newly created appointment to slot
* based on slotId
*
*/
[Authorize]
[HttpPost]
[Route("/appt/assignAppt")]
public string assignAppt([FromBody] dynamic apptData)
{
int id = apptData.SlotId;
string json = apptData.ApptJson;
DateTime timeStamp = DateTime.Now;
using (_context)
{
var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);
// make sure there isn't already an appointment booked in appt slot
if (slot.Timestamp == null)
{
slot.ApptJson = json;
slot.Timestamp = timeStamp;
_context.SaveChanges();
return "Task Executed\n";
}
else
{
return "There is already an appointment booked for this slot.\n" +
"If this slot needs changing try updating it instead of assigning it.";
}
}
}
}
UnitTest.cs
using System;
using Xunit;
using AppointmentAPI.Controllers;
using AppointmentAPI.Appt_Models;
using Microsoft.EntityFrameworkCore;
namespace XUnitTest
{
public abstract class UnitTest1
{
protected UnitTest1(DbContextOptions<ApptSystemContext> contextOptions)
{
ContextOptions = contextOptions;
SeedInMemoryDB();
}
protected DbContextOptions<ApptSystemContext> ContextOptions { get; }
private void SeedInMemoryDB()
{
using(var context = new ApptSystemContext(ContextOptions))
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var seventh = new AppointmentSlots
{
SlotId = 7,
Date = Convert.ToDateTime("2020-05-19 00:00:00.000"),
Time = TimeSpan.Parse("08:45:00.0000000"),
ApptJson = null,
Timestamp = null
};
context.AppointmentSlots.Add(seventh);
context.SaveChanges();
}
}
[Fact]
public void Test1()
{
DbContextOptions<ApptSystemContext> options;
var builder = new DbContextOptionsBuilder<ApptSystemContext>();
builder.UseInMemoryDatabase();
options = builder.Options;
var context = new ApptSystemContext(options);
var controller = new apptController(context);
// Arrange
var request = new AppointmentAPI.Appt_Models.AppointmentSlots
{
SlotId = 7,
ApptJson = "{'fname':'Emily','lname':'Carlton','age':62,'caseWorker':'Brenda', 'appStatus':'unfinished'}",
Timestamp = Convert.ToDateTime("2020-06-25 09:34:00.000")
};
string expectedResult = "Task Executed\n";
// Act
var response = controller.assignAppt(request);
Assert.Equal(response, expectedResult);
}
}
}
InMemoryClass.cs
using System;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using AppointmentAPI.Appt_Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace XUnitTest
{
public class InMemoryClass1 : UnitTest1, IDisposable
{
private readonly DbConnection _connection;
public InMemoryClass1()
:base(
new DbContextOptionsBuilder<ApptSystemContext>()
.UseSqlite(CreateInMemoryDB())
.Options
)
{
_connection = RelationalOptionsExtension.Extract(ContextOptions).Connection;
}
private static DbConnection CreateInMemoryDB()
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
}
public void Dispose() => _connection.Dispose();
}
}

The exception suggests that you haven't registered your DBContext in your Startup.cs (as mentioned above). I'd also suggest that you change the name of your private readonly property to something other than DbContext (which is the class name and can get confusing)
Use something like this:
private readonly ApptSystemContext _context;
Besides that, your approach should be changed.
First, you will set the connection string when you register the DBContext. Just let dependency injection take care of that for you. Your controller should look like this:
public apptController(ApptSystemContext dbContext)
{
_context = dbContext;
}
The dbContext won't be null if you register it in Startup.
Next, unit testing is a tricky concept, but once you write your Unit test, you'll start to understand a little better.
You've said that you want to use the SQL In Memory db for unit testing, which is a good approach (be aware that there are limitations to SQL In Mem like no FK constraints). Next, I assume you want to test your Controller, so, since you MUST pass in a DBContext in order to instantiate your Controller, you can create a new DBContext instance that is configured to use the In Memory Database.
For example
public void ApptControllerTest()
{
//create new dbcontext
DbContextOptions<ApptSystemContext> options;
var builder = new DbContextOptionsBuilder<ApptSystemContext>();
builder.UseInMemoryDatabase();
options = builder.Options;
var context = new ApptSystemContext(options);
//instantiate your controller
var controller = new appController(context);
//call your method that you want to test
var retVal = controller.assignAppt(args go here);
}
Change the body of the method to this:
public string assignAppt([FromBody] dynamic apptData)
{
int id = apptData.SlotId;
string json = apptData.ApptJson;
DateTime timeStamp = DateTime.Now;
using (_context)
{
var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);
// make sure there isn't already an appointment booked in appt slot
if (slot.Timestamp == null)
{
slot.ApptJson = json;
slot.Timestamp = timeStamp;
_context.SaveChanges();
return "Task Executed\n";
}
else
{
return "There is already an appointment booked for this slot.\n" +
"If this slot needs changing try updating it instead of assigning it.";
}
}
}
Another suggestion, don't use a dynamic object as the body of a request unless you are absolutely forced to do so. Using a dynamic object allows for anything to be passed in and you lose the ability to determine if a request is acceptible or not.

Related

How to mock DbSet using xUnit and NSubstitute?

I started using xUnit and NSubstitute for my unit tests. I want to mock the following method.
public async Task<DecorationModel> GetDecorationWithId(string userId, string decorationId)
{
var decoration = await _db.Decorations
.Include(d => d.BgImage)
.FirstOrDefaultAsync(d => d.Id == decorationId);
if (decoration == null || decoration.OwnerId != userId)
return null;
return new DecorationModel
{
Id = decoration.Id,
Name = decoration.Name,
// Other stuff
};
}
I attempted it but couldn't get it to work. My current test class is as follows;
public class DecorationServiceTests
{
private readonly DecorationService _subject;
private readonly IAppDbContext _db = Substitute.For<IAppDbContext>();
private readonly DbSet<Decoration> _decorationDbSet = Substitute.For<DbSet<Decoration>, IQueryable<Decoration>>();
public DecorationServiceTests()
{
_subject = new DecorationService(_db);
}
[Fact]
public async Task GetDecorationWithId_ShouldReturnDecoration_WhenExists()
{
// Arrange
var userId = new Guid().ToString();
var decorationId = new Guid().ToString();
var decorations = new List<Decoration>()
{
new Decoration()
{
Id = decorationId,
Name = "",
OwnerId = userId,
}
};
_db.Decorations.Returns(_decorationDbSet);
_decorationDbSet.FirstOrDefaultAsync(t => t.Id == decorationId).Returns(decorations.FirstOrDefault());
// Act
var result = await _subject.GetDecorationWithId(userId, decorationId);
// Assert
Assert.Equal(result.Id, decorations[0].Id);
}
}
However, I get the following error:
"The provider for the source 'IQueryable' doesn't implement 'IAsyncQueryProvider'. Only providers that implement 'IAsyncQueryProvider' can be used for Entity Framework asynchronous operations."
I searched on the web but couldn't find a good reference. How can I solve this?
I think you'll be going through a whole lot of pain and suffering if you are trying to mock DbSet. That comes straight from the docs of EFCore: https://learn.microsoft.com/en-us/ef/core/testing/#unit-testing
Instead, you should be trying to use a real db or in-memory one.
See the testing sample here: https://learn.microsoft.com/en-us/ef/core/testing/testing-sample

Mock not behaving as expected when setup

I am trying to test a method with the name GetAll in a xUnit-Project of the class AuhtorRepository.
public class AuthorRepository : IAuthorRepository
{
private readonly ISqlDb _db;
public string Connection { get; set; }
public AuthorRepository(ISqlDb db)
{
_db = db;
}
public async Task<List<AuthorModel>> GetAll()
{
string sql = "SELECT * FROM Author";
List<AuthorModel> authors = await _db.LoadDataAsync<AuthorModel, dynamic>(sql, new { }, Connection);
return authors;
}
}
In this method is the LoadDataAsync method used which I will mock in my test.
The structure of the interface looks like this.
public interface ISqlDb
{
Task<List<T>> LoadDataAsync<T, U>(string sql, U parameters, string connection);
}
Finally the implementation of the test my xUnit-Project.
using Autofac.Extras.Moq;
using DataAccess.Library;
using Moq;
using Repository.Library.Models;
using Repository.Library.Repositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace RespositoryTests
{
public partial class AuhtorRepositoryTests
{
[Fact]
public async Task GetAll_ShouldWorkd()
{
using (var autoMock = AutoMock.GetLoose())
{
//Arrange
autoMock.Mock<ISqlDb>()
.Setup(x => x.LoadDataAsync<AuthorModel, dynamic>("SELECT * FROM Author", new { }, " "))
.ReturnsAsync(GetSamples());
//Act
var cls = autoMock.Create<AuthorRepository>();
cls.Connection = " ";
var expected = GetSamples();
var acutal = await cls.GetAll();
//Assert
Assert.Equal(expected.Count, acutal.Count);
}
}
private List<AuthorModel> GetSamples()
{
var authors = new List<AuthorModel>();
authors.Add(new AuthorModel { Id = 1, FirstName = "first name", LastName = "lastname" });
authors.Add(new AuthorModel { Id = 3, FirstName = "Angela", LastName = "Merkel" });
return authors;
}
}
}
The sturcture of the project looks like this:
[1]: https://i.stack.imgur.com/F4TPT.png
The test fails with the following statement:
Message: System.NullReferenceException : Object reference not set to an instance of an object.
So I expected the test should pass.
What I tried out so far:
-> I wrote a similar project with the same code, the only difference there is the project structure. Every interface and class was in the same Test-Project and the test passed.
Whereas in my BookApp Solution the ISqlDb is suited in the DataAccess.Library Project.
So it looks like that in the mocking the ReturnAsync method is not returning the values
from my GetSamples.
Please bear in mind that
a) This is my first post on stackoverflow and
b) I try to make my first steps in mocking ...
tried out:
//Act
//var cls = autoMock.Create<AuthorRepository>();
//cls.Connection = "";
var cls = new AuthorRepository(autoMock.Create<ISqlDb>());
cls.Connection = "";
var expected = GetSamples();
var acutal = await cls.GetAll();
//Assert
Assert.Equal(expected.Count, acutal.Count);
The mock is returning null by default because the invocation of the mocked member does not match what was setup. Reference Moq Quickstart to get a better under standing of how to use MOQ.
You need to use argument matchers here because the anonymous new { } used in setup wont match the reference of the one actually used when exercising the test
//...
autoMock.Mock<ISqlDb>()
.Setup(x => x.LoadDataAsync<AuthorModel, dynamic>("SELECT * FROM Author", It.IsAny<object>(), It.IsAny<string>()))
.ReturnsAsync(GetSamples());
//...
Note the use of It.IsAny<object>() to allow whatever you pass in to be allowed.
With the above change, the test passed as expected.

Unit testing EF Core using in-memory database with an eager-loaded function

I am writing unit tests for my my Web API and cannot get the test to pass except by removing the include (eager-loading from the method). I am using the in-memory database to provide the dbcontext and can't figure out why it is returning no data. Thanks in advance for any help or constructive criticism
This is the method I am trying to test.
Note: it passes the test if I comment out the .include statements.
public async Task<LibraryAsset> GetAsset(int assetId)
{
var asset = await _context.LibraryAssets
.Include(p => p.Photo)
.Include(p => p.Category)
.Include(a => a.AssetType)
.Include(s => s.Status)
.Include(s => s.Author)
.FirstOrDefaultAsync(x => x.Id == assetId);
return asset;
}
This is the base DbContext using the inMemory DB:
public DataContext GetDbContext()
{
var builder = new DbContextOptionsBuilder<DataContext>();
if (useSqlite)
{
// Use Sqlite DB.
builder.UseSqlite("DataSource=:memory:", x => { });
}
else
{
// Use In-Memory DB.
builder.UseInMemoryDatabase(Guid.NewGuid().ToString());
}
var DataContext = new DataContext(builder.Options);
if (useSqlite)
{
// SQLite needs to open connection to the DB.
// Not required for in-memory-database and MS SQL.
DataContext.Database.OpenConnection();
}
DataContext.Database.EnsureCreated();
return DataContext;
}
This is the test:
[Fact]
public async void GetAssetById_ExistingAsset_ReturnAsset()
{
using (var context = GetDbContext())
{
ILogger<LibraryAssetService> logger = new
NullLogger<LibraryAssetService>();
var service = new LibraryAssetService(context, _logger);
var asset = new LibraryAsset
{
Id = 40,
NumberOfCopies = 20,
Title = "",
Year = 1992,
Status = new Status { Id = 1 },
AssetType = new AssetType { Id = 1 },
Author = new Author { Id = 1 },
Category = new Category { Id = 2 },
Photo = new AssetPhoto { Id = 1 }
};
context.LibraryAssets.Attach(asset);
context.Add(asset);
context.SaveChanges();
var actual = await service.GetAsset(40);
Assert.Equal(40, actual.Id);
}
}
This is my first time writing unit tests and I am basically learning as I go. Please feel free to point out any other mistakes that you may have noticed as well.
There are some issues with your code:
If your real databse is relational avoid using UseInMemoryDatabase database for testing because it doesn't support relational behaviours.
Separate the Arrange contexts from the Act contexts. That means, create a new DataContext for preparing the test, adding test data, and etc, and create another one for SUT (LibraryAssetService in this case). DbContext stores local data (in memory) which may not exist in the database and that could show fake green tests in some scenarios!
You don't need Attach when you're adding the assets. That could create Foreign key constraint error with sqlite.
I removed some of your navigations and parameters for the sake of simplicity. So lets suppose the LibraryAssetService is something like this:
public class LibraryAssetService
{
public LibraryAssetService(DataContext context)
{
_context = context;
}
private readonly DataContext _context;
public async Task<LibraryAsset> GetAsset(int assetId)
{
var asset = await _context.LibraryAssets
.Include(p => p.Photo)
.Include(s => s.Author)
.FirstOrDefaultAsync(x => x.Id == assetId);
return asset;
}
}
The test class:
public class LibraryAssetServiceTests
{
public LibraryAssetServiceTests()
{
_factory = new TestDataContextFactory();
}
private TestDataContextFactory _factory;
[Fact]
public async void GetAssetById_ExistingAsset_ReturnAsset()
{
// Arrange
using (var context = _factory.Create())
{
var asset = new LibraryAsset
{
Id = 40,
Author = new Author { Id = 1 },
Photo = new Photo { Id = 1 }
};
context.Add(asset);
context.SaveChanges();
}
// Act
using (var context = _factory.Create())
{
var service = new LibraryAssetService(context);
var actual = await service.GetAsset(40);
// Assert
Assert.Equal(40, actual.Id);
Assert.Equal(1, actual.Author.Id);
Assert.Equal(1, actual.Photo.Id);
}
}
}
And finally, a little helper class to prepare the DataContext for your tests. It's good practice to extract these kind of things outside your test classes. The important thing to remember when testing with sqlite memory databases is that you should keep the connection open during the test. No matter how many DbContext instances you create. The xUnit create an instance of the test class for each test method. So an instance of TestDataContextFactory will be created for each test, and you are good to go.
public class TestDataContextFactory
{
public TestDataContextFactory()
{
var builder = new DbContextOptionsBuilder<DataContext>();
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
builder.UseSqlite(connection);
using (var ctx = new DataContext(builder.Options))
{
ctx.Database.EnsureCreated();
}
_options = builder.Options;
}
private readonly DbContextOptions _options;
public DataContext Create() => new DataContext(_options);
}

Entity Framework mocking requires global context

I have recently began to dig into Entity Framework unit-testing with Entity Framework 6 mocking.
I have noticed the following thing:
Entity Framework mocking forces me to create a global context in my BL class, for example:
public class RefundRepayment : IDisposable
{
protected DbContext _dbContext = new DbContext();
/* more properties and class code */
public void Dispose()
{
_dbContext.Dispose();
}
}
I can't quite figure it out, as I'd rather implement the using statement in every method in order to deal with the DbContext, my code will look like:
public class RefundRepayment
{
/* more properties and class code */
public void AccessDb()
{
using(DbContext dbContext = new DbContext())
{
/* db code here */
}
}
}
Is there any specific reason why should we initialize a global context instead of implementing the using statement?
First off, you need to be using DI (via ninject, Unity, Core, etc) to pull this off.
Let me show you a simple sample of an EF GetAll() testing my MVC controller.
[Fact]
public void GetAllOk()
{
// Arrange
// Act
var result = _controller.GetAll() as OkObjectResult;
// Assert
Assert.NotNull(result);
var recordList = result.Value as List<DTO.Account>;
Assert.NotNull(recordList);
Assert.Equal(4, recordList.Count);
}
It relies on this startup code...
public class AccountsControllerTests
{
DatabaseFixture _fixture;
AccountsControllerV1 _controller;
public AccountsControllerTests(DatabaseFixture fixture)
{
_fixture = fixture;
_controller = new AccountsControllerV1(_fixture._uow);
}
What is DatabaseFixture? Glad you asked...
public class DatabaseFixture : IDisposable
{
public ApplicationDbContext _context;
public DbContextOptions<ApplicationDbContext> _options;
public IUoW _uow;
public DatabaseFixture()
{
var x = Directory.GetCurrentDirectory();
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.Tests.json", optional : true)
.Build();
_options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "ProviderTests")
.Options;
_context = new ApplicationDbContext(_options);
_context.Database.EnsureCreated();
Initialize();
_uow = new UoW(_context);
}
private void Initialize()
{
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 1", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 2", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 3", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 4", AccountID = "", AccountUniqueID = "" });
_context.SaveChanges();
}
public void Dispose()
{
// Clean Up
_context.Database.EnsureDeleted();
}
}
[CollectionDefinition("Database Collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
}
A few definitions used in the above code. I used a Unit of Work Pattern that contains references to all my EF repositories. I kept Entity (Database) classes and DTO (Data Transfer Object) Classes separate. I used an in-memory replacement for the EF database that I initialize at the beginning of each run and/or test so that my data is always known. I inject the Database Fixture into my test class (not each test) so I am not creating/destroying constantly. Then I create my controller passing in my database UoW definition.
You're real controller requires injection of the UoW container you've created with the real database. You are merely substituting a controlled database environment for your test.
public AccountsControllerV1(IUoW uow)
{
_uow = uow;
}
And yes, I use versioning for the sharp-eyed. And yes, this is a Core 2 example. Still applicable for EF 6, just need 3rd party DI ;)
And the controller method I am testing?
[HttpGet("accounts", Name ="GetAccounts")]
public IActionResult GetAll()
{
try
{
var recordList = _uow.Accounts.GetAll();
List<DTO.Account> results = new List<DTO.Account>();
if (recordList != null)
{
results = recordList.Select(r => Map(r)).ToList();
}
log.Info($"Providers: GetAccounts: Success: {results.Count} records returned");
return Ok(results);
}
catch (Exception ex)
{
log.Error($"Providers: GetAccounts: Failed: {ex.Message}");
return BadRequest($"Providers: GetAccounts: Failed: {ex.Message}");
}
}

How to add caching to my MVC Application and WCF Service

I have an MVC Web application that basically queries a SQL store procedure for a list of products,I have a WCF service layer that is responsible for the database querying, there is a call that gets products by category and return the data to MVC Grid-view. I have managed to cache on the application level by setting outputcache to a duration of 3600, this works fine, however is caches the data only after I make an initial call to each product categories, how can I make it consistent on start-up. Also how do I cache the data in the WCF service layer. Please see my code to see what I have so far. I am fairly new to MVC can you please help.
public class HomeController : Controller
{
[OutputCache(Duration = 3600, Location = OutputCacheLocation.Client, VaryByParam = "none")]
public ActionResult Index()
{
TopProductWCFService.TopProductServiceClient client = new TopProductWCFService.TopProductServiceClient();
List<Top_100_Result> productType = client.GetTopProductsByTypeName();
ViewBag.ProductType = new SelectList(productType.Select(x => x.Product_Type_Name).Distinct().OrderBy(x => x));
return View("Index", productType);
}
[OutputCache(Duration = 3600)]
public ActionResult ProductDescription(string ProductType)
{
TopProductWCFService.TopProductServiceClient client = new TopProductWCFService.TopProductServiceClient();
List<Top_100_Result> productDesctriptionList = client.GetTopProductsByCategory(ProductType).Where(x => x.Product_Type_Name == ProductType).ToList();//new List<Top_100_Result>();
return PartialView("_ProductDescription", productDesctriptionList);
}
}
public class Service1 : ITopProductService
{
//private const string CacheKey = "topProducts";
public List<Top_100_Result> GetTopProductsByTypeName()
{
using (EmbraceEntities ctx = new EmbraceEntities())
{
var productObjects = ctx.Top_100(null);
return new List<Top_100_Result>(productObjects.Distinct());
}
}
public List<Top_100_Result> GetTopProductsByCategory(string productCategory)
{
using (EmbraceEntities ctx = new EmbraceEntities())
{
var productsCategoryList = ctx.Top_100(productCategory);
return new List<Top_100_Result>(productsCategoryList);
}
}
}
To cache data from WCF service, you should have a Cache layer first. Sample code:
using System.Runtime.Caching;
public class CacheManager
{
private static MemoryCache _cache = MemoryCache.Default;
public static void AddToCache<T>(string key, T value)
{
_cache[key] = value;
}
public static T GetFromCache<T>(string key)
{
return (T)_cache[key];
}
public static void RemoveFromCache(string key)
{
_cache.Remove(key);
}
}
Then use it in your data layer, eg:
public List<Top_100_Result> GetTopProductsByTypeName()
{
var products = CacheManager.GetFromCache<List<Top_100_Result>>("TOP_100_RESULT");
//Add to cache if not existed
if (products == null)
{
using (EmbraceEntities ctx = new EmbraceEntities())
{
var productObjects = ctx.Top_100(null);
products = new List<Top_100_Result>(productObjects.Distinct());
CacheManager.AddToCache<List<Top_100_Result>>("TOP_100_RESULT", products);
}
}
return products;
}
You should also clear cache to refresh the data as soon as the cache data becomes invalid.
CacheManager.RemoveFromCache("TOP_100_RESULT");
There are many ways how you can do that. Maybe one would be:
(Pseudo code)
at the global.asax.cs
public static Service1 MyService = new Service1();
protected void Application_Start()
{
Task.CreateNew(()=>mySerivce.Init());
I would initalize your service in a task. At the init i would read the Entities() and cache them locally.
GetTopProductsByTypeName()
{
return new List<Top_100_Result>(productObjectsCache.Distinct());
then you need a Update Method when the data object are changed.
public ActionResult Index()
{
List<Top_100_Result> productType = WebHostApplication.MyService.GetTopProductsByTypeName();

Categories