Search code examples
c#unit-testingasp.net-core-mvcintegration-testing

C# can't access an in-memory database created for integration tests


I have a C# / ASP.NET Core MVC app for which I'm writing integration tests. I've been following https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0 and I created a CustomWebApplicationFactory like this in the following file:

CustomWebApplicationFactory.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using amaranth.Data;

namespace amaranth.Tests
{
    #region snippet1
    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));

                services.Remove(descriptor);

                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });

                services.AddAntiforgery(t =>
                {
                    t.Cookie.Name = AntiForgeryTokenExtractor.AntiForgeryCookieName;
                    t.FormFieldName = AntiForgeryTokenExtractor.AntiForgeryFieldName;
                });

                var sp = services.BuildServiceProvider();

                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                    db.Database.EnsureCreated();
                }
            });
        }
    }
    #endregion
}

I also have the following helper files:

Helpers/HtmlHelpers.cs

Helpers/using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Html.Dom;
using AngleSharp.Io;

namespace amaranth.Tests.Helpers
{
    public class HtmlHelpers
    {
        public static async Task<IHtmlDocument> GetDocumentAsync(HttpResponseMessage response)
        {
            var content = await response.Content.ReadAsStringAsync();
            var document = await BrowsingContext.New()
                .OpenAsync(ResponseFactory, CancellationToken.None);
            return (IHtmlDocument)document;

            void ResponseFactory(VirtualResponse htmlResponse)
            {
                htmlResponse
                    .Address(response.RequestMessage.RequestUri)
                    .Status(response.StatusCode);

                MapHeaders(response.Headers);
                MapHeaders(response.Content.Headers);

                htmlResponse.Content(content);

                void MapHeaders(HttpHeaders headers)
                {
                    foreach (var header in headers)
                    {
                        foreach (var value in header.Value)
                        {
                            htmlResponse.Header(header.Key, value);
                        }
                    }
                }
            }
        }
    }
}

Helpers/HttpClientExtensions.cs

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Html.Dom;
using Xunit;

namespace amaranth.Tests.Helpers
{
    public static class HttpClientExtensions
    {
        public static Task<HttpResponseMessage> SendAsync(
            this HttpClient client,
            IHtmlFormElement form,
            IHtmlElement submitButton)
        {
            return client.SendAsync(form, submitButton, new Dictionary<string, string>());
        }

        public static Task<HttpResponseMessage> SendAsync(
            this HttpClient client,
            IHtmlFormElement form,
            IEnumerable<KeyValuePair<string, string>> formValues)
        {
            var submitElement = Assert.Single(form.QuerySelectorAll("[type=submit]"));
            var submitButton = Assert.IsAssignableFrom<IHtmlElement>(submitElement);

            return client.SendAsync(form, submitButton, formValues);
        }

        public static Task<HttpResponseMessage> SendAsync(
            this HttpClient client,
            IHtmlFormElement form,
            IHtmlElement submitButton,
            IEnumerable<KeyValuePair<string, string>> formValues)
        {
            foreach (var kvp in formValues)
            {
                var element = Assert.IsAssignableFrom<IHtmlInputElement>(form[kvp.Key]);
                element.Value = kvp.Value;
            }

            var submit = form.GetSubmission(submitButton);
            var target = (Uri)submit.Target;
            if (submitButton.HasAttribute("formaction"))
            {
                var formaction = submitButton.GetAttribute("formaction");
                target = new Uri(formaction, UriKind.Relative);
            }
            var submission = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), target)
            {
                Content = new StreamContent(submit.Body)
            };

            foreach (var header in submit.Headers)
            {
                submission.Headers.TryAddWithoutValidation(header.Key, header.Value);
                submission.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }

            return client.SendAsync(submission);
        }
    }
}

The app I'm testing is called amaranth and my testing project is called amaranth.Tests. In the amaranth controller AdminController.cs, I am trying to test the function CreateMasterWallet which looks like this:

[HttpPost]
public IActionResult CreateMasterWallet(bool isTestNet, string label)
{
    _db.MasterWallets.Add(new MasterWallet
    {
        Label = label,
        Address = BitcoinHelper.CreatePrivateKey(isTestNet),
        IsTestNet = isTestNet
    });

    _db.SaveChanges();

    return RedirectToAction(nameof(MasterWalletList));
}

I simply need to write a test to make sure that a master wallet is successfully created and added to my in memory database but I don't know how to access my in memory database for the test. This was my best attempt at writing a test that checks to see if a master wallet is being successfully added to the database:

[Fact]
public void CreateMasterWalletTest()
{
    var _db = _factory.db;
    _db.MasterWallets.Add(new MasterWallet
    {
        Label = label,
        Address = BitcoinHelper.CreatePrivateKey(isTestNet),
        IsTestNet = isTestNet
    });
    Assert.True(_db.Count() > 0);
}

It fails with this error:

/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(18,32): error CS1061: 'CustomWebApplicationFactory<Startup>' does not contain a definition for 'db' and no accessible extension method 'db' accepting a first argument of type 'CustomWebApplicationFactory<Startup>' could be found (are you missing a using directive or an assembly reference?) [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]
/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(19,39): error CS0246: The type or namespace name 'MasterWallet' could not be found (are you missing a using directive or an assembly reference?) [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]
/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(21,25): error CS0103: The name 'label' does not exist in the current context [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]
/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(22,27): error CS0103: The name 'BitcoinHelper' does not exist in the current context [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]
/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(22,58): error CS0103: The name 'isTestNet' does not exist in the current context [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]
/path/to/directory/amaranthPost052422Dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs(23,29): error CS0103: The name 'isTestNet' does not exist in the current context [/path/to/directory/amaranthPost052422Dir/amaranth.Tests/amaranth.Tests.csproj]

So how do I write a test to ensure that CreateMasterWallet is successfully creating a new MasterWallet and saving to the database?


Solution

  • To retrieve the database you've registered with the webhost builder, you can do something like this:

    using (var scope = _factory.Host.Services.CreateScope())
    {
        _db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    }
    

    The CustomWebApplicationFactory doesn't know that you call that ApplicationDbContext db, and doesn't have a field like that.

    For the other build errors: You probably also need to create some constants or other variables for label and isTestNet.

    BitcoinHelper is probably in an unreferenced namespace, same with MasterWallet. You might check that you have the right references in your test project, and that you've got a using statement at the top of your test file referencing their namespaces. You can also use Visual Studio's right click menu to add the reference/using statement if it recognizes those types.