DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. Integration Testing Data Access in ASP.NET Core

Integration Testing Data Access in ASP.NET Core

Integration tests an integral part of the process when it comes to making sure your code is up to snuff. Learn how to perform integration testing in ASP.NET Core!

Juergen Gutsch user avatar by
Juergen Gutsch
·
Jan. 29, 19 · Tutorial
Like (6)
Save
Tweet
Share
17.50K Views

Join the DZone community and get the full member experience.

Join For Free

In the last post, I wrote about unit testing data access in ASP.NET Core. This time I'm going to go into integration tests. This post shows you how to write an end-to-end test using a WebApplicationFactory and hot to write a specific integration test.

Unit Tests vs. Integration Tests

I'm sure most of you already know the difference. In a few discussions, I learned that some developers don't have a clear idea about the difference. In the end, it doesn't really matter, because every test is a good test. Both unit tests and integration tests are coded tests, they look similar, and use the same technology. The difference is in the concepts of how and what to test and in the scope of the test:

  • A unit test tests a logical unit, a single isolated component, a function, or a feature. A unit test isolates this component to test it without any dependencies like I did in the last post. First, I tested the actions of a controller, without testing the actual service in behind. Then I tested the service methods in an isolated way with a faked DbContext. Why? Because unit tests shouldn't break because of a failing dependency. A unit test should be fast in development and in execution. It is a development tool. So it shouldn't cost a lot of time to write one. And, in fact, setting up a unit test is much cheaper than setting up an integration test. Usually, you write a unit test during or immediately after implementing the logic. In the best case, you'll write a unit test before implementing the logic. This would be the TDD way, test-driven development or test-driven design.
  • An integration test does a lot more. It tests the composition of all units. It ensures that all units are working together in the right way. This means it may need a lot more effort to set up a test because you need to set up the dependencies. An integration test can test a feature from the UI to the database. It integrates all the dependencies. On the other hand, an integration test can be isolated on a hot path of a feature. It is also legit to fake or mock aspects that don't need to be tested in this special case. For example, if you test a user input from the UI to the database, you don't need to test the logging. Also, an integration test shouldn't fail because on an error outside the context. This also means to isolate an integration test as much as possible, maybe by using an in-memory database instead of a real one.

Let's see how it works.

Set Up

I'm going to reuse the solution created for the last post to keep this section short.

I only need to create another XUnit test project, to add it to the existing solution and to add a reference to the WebToTest and some NuGet packages:

dotnet new xunit -o WebToTest.IntegrationTests -n WebToTest.IntegrationTests
dotnet sln add WebToTest.IntegrationTests
dotnet add WebToTest.IntegrationTests reference WebToTest

dotnet add WebToTest.IntegrationTests package GenFu
dotnet add WebToTest.IntegrationTests package moq
dotnet add WebToTest.IntegrationTests package Microsoft.AspNetCore.Mvc.Testing

In the next step, I create a test class for a web integration test. This means I set up a web host for the application-to-test to call the web via a web client. This is kind of a UI test then, not based on UI events, but I'm able to get and analyze the HTML result of the page to test.

Since ASP.NET Core 2.0, we've had the ability to set up a test host to run on the web in the test environment. This is pretty cool. You don't need to set up an actual web server to run a test against the web. This gets done automatically by using the generic WebApplicationFactory. You just need to specify the type of the startup class of the web-to-test:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace WebToTest.IntegrationTests
{
    public class PersonTests : IClassFixture<WebApplicationFactory<Startup>>
    {
        private readonly WebApplicationFactory<Startup> _factory;

        public PersonTests(WebApplicationFactory<Startup> factory)
        {
            _factory = factory;
        }

        // put test methods here
    }
}

Also, the XUnit IClassFixture is special here. This generic interface tells XUnint to create an instance of the generic argument per test to run. In this case, I get a new instance of the WebApplicationFactory of Startup per test. Wow, this test class created its own web server every time a test method got executed! This is an isolated test environment per test.

End-to-End tests

Our first integration tests will ensure the MVC routes are working. This test creates a web host and calls the web via HTTP. It tests parts of the application from the UI to the database. This is an end-to-end test.

Instead of an XUnit Fact, we create a Theory this time. A Theory marks a test method which is able to retrieve input data via an attribute. The InlineDataAttribute defines the data we want to pass in. In this case, the MVC route URLs:

[Theory]
[InlineData("/")]
[InlineData("/Home/Index")]
[InlineData("/Home/Privacy")]
public async Task BaseTest(string url)
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync(url);

    // Assert
    response.EnsureSuccessStatusCode(); // Status Code 200-299
    Assert.Equal("text/html; charset=utf-8",
        response.Content.Headers.ContentType.ToString());
}

Let's try it:

dotnet test WebToTest.IntegrationTests

This actually creates three test results as you can see in the output window:

We'll now need to do the same thing for the API routs. Why did I do this in a separate method? Because the first integration test also checks the content type, which is the type of an HTML document. The content type of the API results is application/json:

[Theory]
[InlineData("/api/person")]
[InlineData("/api/person/1")]
public async Task ApiRouteTest(string url)
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync(url);

    // Assert
    response.EnsureSuccessStatusCode(); // Status Code 200-299
    Assert.Equal("application/json; charset=utf-8",
        response.Content.Headers.ContentType.ToString());
}

This also works and we have to more successful tests now:

This isn't completely isolated, because it uses the same database as the production or the test web. At least it is the same file-based SQLite database as in the test environment. Because a test should be as fast as possible, wouldn't it make sense to use an in-memory database instead?

Usually, it would be possible to override the service registration of the Startup.cswith the WebApplicationFactory we retrieve in the constructor. It should be possible to add the ApplicationDbContext and to configure an in-memory database:

public PersonTests(WebApplicationFactory<Startup> factory)
{
    _factory = factory.WithWebHostBuilder(config =>
    {
        config.ConfigureServices(services =>
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseInMemoryDatabase("InMemory"));
        });
    });
}

Unfortunately, I didn't get the seeding running for the in-memory database using the current preview version of ASP.NET Core 3.0. This will result in a failing test for the route URL /api/person/1 because the Person with the Id1 isn't available. This is a known issue on GitHub.

To get this running, we need to ensure seeding explicitly every time we create an instance of the dbContext.

public PersonService(ApplicationDbContext dbContext)
{
    _dbContext = dbContext;
    _dbContext.Database?.EnsureCreated();
}

This hopefully gets fixed, because it is kind of bad to add this line only for the integration tests. Anyway, it works this way. Maybe you find a way to call EnsureCreated() in the test class.

Specific Integration Tests

Sometimes it makes sense to test more specific parts of the application, without starting a web host and without accessing a real database, just to be sure that the individual units are working together. This time I'm testing the PersonController together with the PersonService. I'm going to mock the DbContext, because the database access isn't relevant for the test. I just need to ensure the service provides the data to the controller in the right way and to ensure the controller is able to handle this data.

At first, I create a simple test class that is able to create the necessary test data and the DbContext mock:

public class PersonIntegrationTest
{
    // put the tests here

    private Mock<ApplicationDbContext> CreateDbContextMock()
    {
        var persons = GetFakeData().AsQueryable();

        var dbSet = new Mock<DbSet<Person>>();
        dbSet.As<IQueryable<Person>>().Setup(m => m.Provider).Returns(persons.Provider);
        dbSet.As<IQueryable<Person>>().Setup(m => m.Expression).Returns(persons.Expression);
        dbSet.As<IQueryable<Person>>().Setup(m => m.ElementType).Returns(persons.ElementType);
        dbSet.As<IQueryable<Person>>().Setup(m => m.GetEnumerator()).Returns(persons.GetEnumerator());

        var context = new Mock<ApplicationDbContext>();
        context.Setup(c => c.Persons).Returns(dbSet.Object);

        return context;
    }

    private IEnumerable<Person> GetFakeData()
    {
        var i = 1;
        var persons = A.ListOf<Person>(26);
        persons.ForEach(x => x.Id = i++);
        return persons.Select(_ => _);
    }
}

Next, I wrote the tests, which look similar to to the PersonControllerTests I wrote in the last blog post. Only the arranged part differs a little bit. This time I don't pass the mocked service in, but an actual one that uses a mocked DbContext:

[Fact]
public void GetPersonsTest()
{
    // arrange
    var context = CreateDbContextMock();

    var service = new PersonService(context.Object);

    var controller = new PersonController(service);

    // act
    var results = controller.GetPersons();

    var count = results.Count();

    // assert
    Assert.Equal(26, count);
}

[Fact]
public void CreateDbContextMock()
{
    // arrange
    var context = CreateDbContext();

    var service = new PersonService(context.Object);

    var controller = new PersonController(service);

    // act
    var result = controller.GetPerson(1);
    var person = result.Value;

    // assert
    Assert.Equal(1, person.Id);
}

Let's try it by using the following command:

dotnet test WebToTest.IntegrationTests

Et voilà:

At the end, we should run all the tests of the solution at once to be sure we don't break the existing tests and the existing code. Just type dotnet test and see what happens:

Conclusion

I wrote that integration tests will cost a lot more effort than unit tests. This isn't completely true since we are able to use the WebApplicationFactory. In many other cases, it will be a little more expensive, depending on how you want to test and how many dependencies you have. You need to figure out how you want to isolate an integration test. More isolation sometimes means more effort, less isolation means more dependencies that may break your test.

Anyway, writing integration tests, in my point of view, is more important than writing unit tests, because it tests that the parts of the application are working together. And it is not that hard and doesn't cost that much.

Just do it. If you never wrote tests in the past, try it. It feels great to be on the safe way, to be sure your code is working as expected.

unit test Integration ASP.NET ASP.NET Core Data (computing) Data access Database integration test Integration testing Web Service

Published at DZone with permission of Juergen Gutsch, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Java Development Trends 2023
  • Using AI and Machine Learning To Create Software
  • Kubernetes vs Docker: Differences Explained
  • RabbitMQ vs. Memphis.dev

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: