DEV Community

Carl Layton
Carl Layton

Posted on

Unit Testing Azure Functions With EF

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.

create-azure-function-csproj

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.

class-lib-for-data-context-csproj

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.

create-nunit-csproj

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; }
}
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"]));
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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; } 
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}   
Enter fullscreen mode Exit fullscreen mode

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)