Introduction
In this post I demonstrate how to create and unit test an Azure function app that uses Entity Framework. I only debug the function app running on the local machine and never publish it to Azure, so no Azure subscription is required. This example uses Visual Studio 2022, .NET 6, Entity Framework 6.0.x, NUnit 3.x, localdb, and the azure functions tools with Visual Studio. The full example is available in GitHub.
Setup projects
There are 3 projects, the azure function, the data project that contains the data context and Entity Framework dependencies, and the unit test project. First, I'm going to create a shell of each project and then go into detail of each in the following sections. All 3 projects use .NET 6. To create the Azure Function project, open Visual Studio 2022 and search for the Azure Function template. If you don't see it in the list, you may need to install the Azure development features of Visual Studio.
Select next, give it a name, select create. I named mine AzFuncUnitTestWithEf. Choose Http Trigger and click create again. After the Azure function app is created, it is time to create the class library project for the data context. Right click on the solution and choose Add / New Project. Choose class library for the type and click Next. I named mine AzFuncUnitTestWithEf.DataContext but you can name it whatever you prefer.
Finally, create the unit test project. I'm using NUnit, which has a template in Visual Studio 2022. I'm not sure if there are additional dependencies required for the template to be available. Right click on the solution and, once again, choose Add / New Project, search for nunit, and select NUnit Test Project from the list of templates.
Now that all projects are added, ensure the solution builds. In the next section, I'll go over creating the data context with Entity Framework and localdb.
Entity Framework Setup
In this section, I'll detail adding the required NuGet dependencies for Entity Framework, and creating the model and data context. I do not cover a migration to create a real database because we're not using a real database in this example. For more information on EF migrations, visit this Microsoft doc.
The first step is to add the required NuGet dependencies. These are all installed to the AzFuncUnitTestWithEf.DataContext project. Open Tools / NuGet Package Manager / Managed NuGet Packages for Solution. Below is a table of the Entity Framework NuGet packages and which version to install.
NuGet Package | Version |
---|---|
Microsoft.EntityFrameworkCore | 6.0.4 |
Microsoft.EntityFrameworkCore.SqlServer | 6.0.4 |
Next I'll create the model class and data context. This app uses a simple model class named book. The Book.cs
file is below
public class Book
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column(TypeName = "nvarchar(512)")]
public string Title { get; set; }
[Column(TypeName = "nvarchar(512)")]
public string Author { get; set; }
public DateTime PublishedDate { get; set; }
}
Create another class for the data context named BookDataContext.cs
. This inherits from Entity Framework DbContext
and contains a DbSet
for Books.
public class BookDataContext : DbContext
{
public DbSet<Book> Books { get; set; }
public BookDataContext(DbContextOptions<BookDataContext> options) : base(options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=book_db;Trusted_Connection=True;ConnectRetryCount=0");
}
}
}
At this point, build the project again to make sure the db model and context are correct. In the next section, I'll go over setting up the azure function start up class and using the data context in the azure function.
Add Function App StartUp
To use the data context in the Azure function, I'm going to inject it as a dependency. Like an ASP .NET core web app, an azure function can have a StartUp
class that inherits from FunctionsStartup
to configure services. Browse to Tools / NuGet Package Manager / Manage NuGet Packages for Solution. Install the NuGet packages below to the function app project to use the function startup class.
NuGet Package | Version |
---|---|
Microsoft.Azure.Functions.Extensions | 1.1.0 |
There is 1 method to implement, void Configure(IFunctionsHostBuilder builder)
.We're adding 1 service and that is a DbContext
for the BookDataContext
. The StartUp
class is below.
public class StartUp : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var services = builder.Services;
var configuration = builder.GetContext().Configuration;
services.AddDbContext<BookDataContext>(options => options.UseSqlServer(configuration["DbConnection"]));
}
}
Next, a FunctionStartupAttribute
needs to be assigned in the StartUp
class. Do this by adding this line above the namespace declaration: [assembly: FunctionsStartup(typeof(StartUp))]
. We also need a database connection to configure the DB Context. This is referenced in the code above. I'm using local db and added a setting to the local.settings.json
config file of "DbConnection": "Server=(localdb)\\mssqllocaldb;Database=book_db;Trusted_Connection=True;MultipleActiveResultSets=true"
. We don't actually use this configuration in this demo but in a real project this would be the sql server connection string. The next step is to implement the function to actually do some work and I'll show that in the next section.
Azure Function Implementation
The next step is to implement the azure function to accept requests and save those requests to our database. Since we configured the BookDataContext
as part of the services of the azure function, it can be injected into the azure function constructor through dependency injection. Add a private field and constructor to the azure function. Also, make the azure function class and Run method not static.
private readonly BookDataContext _bookDataContext;
public Function1(BookDataContext ctx)
{
_bookDataContext = ctx;
}
We can now reference _bookDataContext
in the function run method. The function is an HTTP triggered function that accepts POST requests. The POST request contains Book information that is mapped from an input model to a database model and saved through the db context.
[FunctionName("Function1")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger log, ExecutionContext executionContext)
{
try
{
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
var book = await ParseInput(req);
_bookDataContext.Add(Map(book));
await _bookDataContext.SaveChangesAsync();
return new NoContentResult();
}
catch (Exception ex)
{
log.LogError(ex.Message);
return new InternalServerErrorResult();
}
}
The following code is the Parse
and Map
private helper methods to round out the function class.
private async Task<BookInput> ParseInput(HttpRequest req)
{
string requestBody = String.Empty;
using (StreamReader streamReader = new StreamReader(req.Body))
{
requestBody = await streamReader.ReadToEndAsync();
}
return System.Text.Json.JsonSerializer.Deserialize<BookInput>(requestBody);
}
private Book Map(BookInput b)
{
return new Book { Title = b.Title, Author = b.Author, PublishedDate = b.PublishedDate };
}
The request is deserialized to a BookInput
type. That class is below.
public class BookInput
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime PublishedDate { get; set; }
}
That completes the azure function project. Finally, we're ready to setup a unit test that uses an in-memory DB Context and calls the Run
method.
Unit Test Implementation
The last step is to implement tests in the NUnit project that was created earlier. We'll first add a couple NuGet packages. Browse to Tools / NuGet Package Manager / Manage NuGet Packages for Solution. The table below lists the NuGet packages required for the test project.
NuGet Package | Version |
---|---|
Microsoft.EntityFrameworkCore.InMemory | 6.0.5 |
Moq | 4.18.1 |
Once the packages are installed, create a new file named FunctionTests.cs
that will contain our test. We're going to use an in-memory database with Entity Framework for our unit test. This exists for the lifetime that the tests are running. This allows tests to run against Entity Framework without a real database. A static instance of InMemoryDatabaseRoot
is created for the database and a private field is used in the test class for BookDataContext
.
private static InMemoryDatabaseRoot _root = new InMemoryDatabaseRoot();
private BookDataContext _bookContext;
There is a OneTimeSetup
method that is called once prior to all tests running that builds the in-memory database. This is done using a DbContextOptionsBuilder
instance, like building any other database. The UseInMemoryDatabase
extension method tells it to use an in-memory database. Finally, set the _bookDataContext
field to a new BookDataContext
using the db builder options. The one time setup method is below.
[OneTimeSetUp]
public void OneTimeSetup()
{
var dbBuilder = new DbContextOptionsBuilder<BookDataContext>();
dbBuilder.UseInMemoryDatabase(databaseName: "Book", _root);
_bookContext = new BookDataContext(dbBuilder.Options);
}
There is also a setup method that resets the database before each test run.
[SetUp]
public void Setup()
{
// Make sure database is empty when starting
_bookContext.Database.EnsureDeleted();
_bookContext.Database.EnsureCreated();
}
Now lets write a test that uses it. We'll write one test that calls the function's Run method and then verify that it wrote to the in-memory database. First, I'll explain all of the code and then list the full method. The test follows the Arrange, Act, Assert pattern. The arrange section creates the dependencies required for the function run method. This includes a Logger, an executionContext, and test request data.
The next step is to act by instantiating the Function class and passing the local _bookContext
data context to the constructor. Next call the Run
method passing in the local variables for the required parameters. This will execute the full Run method of the function.
Finally, we'll assert the results. This is done by querying the in-memory database for the test Book that was just inserted by the function. Assert the book returned equals the test book data that was passed in the request. If everything checks out, we know the function inserted the book correctly into the database. The full code of the test method is below.
[Test]
public async Task ReturnsSuccess()
{
// Arrange
// Arrange function dependencies
var loggerFactory = new LoggerFactory();
var logger = loggerFactory.CreateLogger("DebugLogger");
var executionContext = new Microsoft.Azure.WebJobs.ExecutionContext();
var id = Guid.NewGuid();
executionContext.InvocationId = id;
// Arrange request data
string input = "{\"Title\": \"Test Title\", \"Author\": \"Test Author\", \"PublishedDate\": \"2022-04-08T14:47:46Z\"}";
using (var requestStream = new MemoryStream())
{
requestStream.Write(Encoding.ASCII.GetBytes(input));
requestStream.Flush();
requestStream.Position = 0;
var requestMock = new Mock<HttpRequest>();
requestMock.Setup(x => x.Body).Returns(requestStream);
// Act
Function1 sut = new Function1(_bookContext);
var result = await sut.Run(requestMock.Object, logger, executionContext);
Book resultData = await _bookContext.Books.FirstOrDefaultAsync(b => b.Title == "Test Title");
//Assert
Assert.AreEqual(typeof(NoContentResult), result.GetType());
Assert.IsNotNull(resultData);
}
}
Conclusion
In conclusion, this post demonstrates how to unit test an azure function that uses entity framework with nUnit and an in-memory EF database. The key is the data context that connects to a database is injected as a dependency so different variations can be used. This allows the test to call the complete Run
method and verify the results against an in-memory database, while the production code would inject a _bookContext
connected to a live database.
Top comments (0)