Effortless Integration Tests with My Tested ASP.NET

Effortless Integration Tests with My Tested ASP.NET

In the perfect scenario, developers should write both unit and integration tests for their web applications and services. The former allows you to write easy and atomic strongly-typed assertions, while the latter gives you system-wide validations at the cost of heavier configuration and string-based statements. Programmers usually love the process of testing and see the benefits, but often they are not provided with the necessary time to assert every component perfectly by their business managers.

My name is Ivaylo Kenov, and I am a guest writer at Automate the Planet. I am currently working as a CTO in a large software development team, and I am a huge fan of high-quality software development and clean code. For this reason, I created My Tested ASP.NET– a collection of open-source fluent testing libraries for ASP.NET.

In this blog post, I would like to show you how to assert ASP.NET Core web applications on a tight schedule – combining the best of both unit and integration tests. We will need xUnit, Moq, Shouldly, and My Tested ASP.NET Core MVC.

Component Under Test

The following code shows our ASP.NET Core controller - ArticlesController.

public class ArticlesController : Controller
{
    private readonly IArticleService articleService;
    public ArticlesController(IArticleService articleService)
    => this.articleService = articleService;
    
    
    public IActionResult Create() => this.View();
    
    
    public async Task<IActionResult> Create(ArticleFormModel article)
    {
        if (this.ModelState.IsValid)
        {
            await this.articleService.Add(article.Title, article.Content, this.User.GetId());
            this.TempData.Add(ControllerConstants.SuccessMessage, "Article created successfully it is waiting for approval!");
            return this.RedirectToAction(nameof(UsersController.Mine), ControllerConstants.Users);
        }
        return this.View(article);
    }
}

And this is our IArticleService.

public class ArticleService : IArticleService
{
    private readonly BlogDbContext db;
    private readonly IDateTimeProvider dateTimeProvider;
    public ArticleService(
    BlogDbContext db,
    IDateTimeProvider dateTimeProvider)
    {
        this.db = db;
        this.dateTimeProvider = dateTimeProvider;
    }
    public async Task<int> Add(string title, string content, string userId)
    {
        var article = new Article
        {
            Title = title,
            Content = content,
            UserId = userId,
            PublishedOn = this.dateTimeProvider.Now();
    };
this.db.Articles.Add(article);
await this.db.SaveChangesAsync();
return article.Id;
    }
}

The IDateTimeProvider interface is a simple service providing the current UTC time:

public class DateTimeProvider : IDateTimeProvider
{
    public DateTime Now() => DateTime.UtcNow;
}

BlogDbContext is just a typical Entity Framework Core DbContext class, which allows us to access our data layer.

The code above is taken from this sample application.

Preparing Our Test Project

To get started with our tests, we can create a xUnit Test Project in Visual Studio, and install MyTested.AspNetCore.Mvc.Universe from NuGet.

Then create a TestStartup class at the root of the test project to register the dependency injection services, which will be used by all test cases in the assembly. This way we will not repeat our code thought our test cases. A quick solution is to inherit from the web project’s Startup class. By default MyTested.AspNetCore.Mvc replaces all ASP.NET Core services with ready to be used mocks. For example, our BlogDbContext database will be automatically replaced with ready for tests in-memory scoped database. We only need to handle our custom services.

For example, the IDateTimeProvider.

// The TestStartup class provides all globally registered services for our tests.
public class TestStartup : Startup
{
    public TestStartup(IConfiguration configuration)
    : base(configuration)
    {
    }
    public void ConfigureTestServices(IServiceCollection services)
    {
        // Register all web application services.
        base.ConfigureServices(services);
        // Replace all custom services which need mocks.
        services.ReplaceTransient<IDateTimeProvider>(_ => DateTimeProviderMock.Create);
    }
}

And this is our DateTimeProviderMock class.

public class DateTimeProviderMock
{
    public static IDateTimeProvider Create
    {
        get
        {
            var moq = new Mock<IDateTimeProvider>();
            moq.Setup(m => m.Now()).Returns(new DateTime(1, 1, 1));
            return moq.Object;
        }
    }
}

We are now ready for our test cases. As you can see, we do not need to mock every single service in our application.

Route Tests

First, let’s assert the routes in our controller. The testing library will get your route configuration from the TestStartup class.

The Get Create action is pretty straightforward.


public void GetCreateShouldBeRoutedCorrectly()
=> MyRouting // Start a route test.
.Configuration() // Use the globally registered configuration.
.ShouldMap(request => request // Provide the request data.
.WithLocation("/Articles/Create") // Set the URL of the Get request.
.WithUser()) // Specify that the route needs an authenticated user.
.To<ArticlesController>(c => c.Create()); // Map the route to the specific action and controller.

The Post Create action needs a little bit more data.


[InlineData("Test Article", "Test Article Content")]
public void PostCreateShouldBeRoutedCorrectly(string title, string content)
=> MyRouting // Start a route test.
.Configuration() // Use the globally registered configuration.
.ShouldMap(request => request // Provide the request data.
.WithMethod(HttpMethod.Post) // Set the method of the request.
.WithLocation("/Articles/Create") // Set the URL of the Post request.
.WithFormFields(new // Add form field data to the request.
{
    Title = title,
    Content = content
})
.WithUser() // Specify that the route needs an authenticated user.
.WithAntiForgeryToken()) // Add an Anti-Forgery token, if needed.
.To<ArticlesController>(c => c.Create(new ArticleFormModel // Map the route to the specific route values.
{
    Title = title,
    Content = content
}));

Controller Tests

Now, let’s assert the controller logic.

The Get Create method needs two tests – one for the action attributes, and one for the view result. We can combine it in a single assertion chain.


public void CreateGetShouldHaveRestrictionsForHttpGetOnlyAndAuthorizedUsersAndShouldReturnView()
=> MyController<ArticlesController> // Specify the controller under test.
    .Instance() // Create it from the globally registered services.
    .Calling(c => c.Create()) // Specify the action under test.
    .ShouldHave()
    .ActionAttributes(attrs => attrs // Assert action attributes.
    .RestrictingForHttpMethod(HttpMethod.Get)
    .RestrictingForAuthorizedRequests())
    .AndAlso() // Provide additional assertions.
    .ShouldReturn()
    .View(); // Validate the view result.

The Post Create method has two execution flows.

If the model state is not valid, we need to assert the returned view result and its model.


public void CreatePostShouldReturnViewWithSameModelWhenInvalidModelState()
=> MyController<ArticlesController> // Specify the controller under test.
    .Instance() // Create it from the globally registered services.
    .Calling(c => c.Create(With.Default<ArticleFormModel>())) // Specify the action under test and provide empty model.
    .ShouldHave()
    .InvalidModelState() // Assert invalid model state.
    .AndAlso() // Provide additional assertions.
    .ShouldReturn()
    .View(With.Default<ArticleFormModel>()); // Validate the view result and the same empty model.

And if the model state is valid, we need to assert the database for an added article, the TempData for a success message and finally, the returned redirect to action result.


[InlineData("Article Title", "Article Content")]
public void CreatePostShouldSaveArticleSetModelStateMessageAndRedirectWhenValidModelState(string title, string content)
=> MyController<ArticlesController> // Specify the controller under test.
    .Instance() // Create it from the globally registered services.
    .Calling(c => c.Create(new ArticleFormModel // Specify the action under test and provide valid model.
    {
        Title = title,
        Content = content
    }))
    .ShouldHave()
    .ValidModelState() // Assert valid model state.
    .AndAlso() // Provide additional assertions.
    .ShouldHave()
    .Data(data => data // Assert the database.
    .WithSet<Article>(set => // Assert the Article database set.
    {
        set.ShouldNotBeEmpty();
        set.SingleOrDefault(a => a.Title == title).ShouldNotBeNull(); // Validate our article is saved.
    }))
    .AndAlso() // Provide additional assertions.
    .ShouldHave()
    .TempData(tempData => tempData // Validate the TempData success message..
    .ContainingEntryWithKey(ControllerConstants.SuccessMessage))
    .AndAlso() // Provide additional assertions.
    .ShouldReturn()
    .Redirect(redirect => redirect // Validate redirect result.
    .To<UsersController>(c => c.Mine())); // Validate correct redirect to action.

Don’t forget the action attributes.


public void CreatePostShouldHaveRestrictionsForHttpPostOnlyAndAuthorizedUsers()
=> MyController<ArticlesController> // Specify the controller under test.
    .Instance() // Create it from the globally registered services.
    .Calling(c => c.Create(With.Empty<ArticleFormModel>())) // Specify the action under test and provide empty model.
    .ShouldHave()
    .ActionAttributes(attrs => attrs // Assert action attributes.
    .RestrictingForHttpMethod(HttpMethod.Post)
    .RestrictingForAuthorizedRequests());

The beauty of these tests is that they assert all layers of our application – the database, the services, and the controller. We hit three rabbits with one shot. And the shot is fired in a fluent, readable, and strongly-typed manner**.**

Pipeline Tests

If you are using My Tested ASP.NET Core MVC for .NET Core 3 or later, you can combine the route and controller tests into a single pipeline test.


public void GetCreateShouldShouldReturnView()
=> MyMvc
    .Pipeline() // Start a pipeline test.
    .ShouldMap(request => request // Provide the request data.
    .WithLocation("/Articles/Create")
    .WithUser())
    .To<ArticlesController>(c => c.Create()) // Validate the route values.
    .Which() // Provide additional assertions on the controller.
    .ShouldReturn()
    .View(); // Validate the view result.

Conclusion

My Tested ASP.NET Core MVC has a very powerful fluent API, providing more than 500 assertion methods – caching, session, authentication, just name it! It sure helps you speed up the testing process in your web development team!

Happy testing, and if you want to read more about the open-source library – this tutorial is the perfect place to start!

Related Articles

AutomationTools, Free Tools, Java

Healenium: Self-Healing Library for Selenium-based Automated Tests

In this article, we're going to review a library called Healenium. It is an AI-powered open-source library for improving the stability of Selenium-based tests,

Healenium: Self-Healing Library for Selenium-based Automated Tests

AutomationTools, Free Tools, Java

Quick Guide Bitbucket Pipelines on Running Selenium Java Tests

In this article from the series Automation Tools, I am going to guide you on how you can set up a Bitbucket Pipelines job for a Selenium Java project, run your

Quick Guide Bitbucket Pipelines on Running Selenium Java Tests

AutomationTools, Free Tools, Web Automation

UI Performance Analysis via Selenium WebDriver

The article from the series Automation Tools reviews different approaches to check the UI performance of web apps reusing your existing functional Selenium WebD

UI Performance Analysis via Selenium WebDriver

AutomationTools, Free Tools

Quick Guide Bitbucket Pipelines on Running Selenium C# Tests

In this article from the series Automation Tools, I am going to guide you on how you can set up a Bitbucket Pipelines job for a Selenium C# project, run your Se

Quick Guide Bitbucket Pipelines on Running Selenium C# Tests

AutomationTools, Free Tools, Java

Quick Guide GitHub Actions on Running Selenium Java Tests

In this article from the series Automation Tools, I am going to guide you on how you can set up a GitHub Actions job for a Selenium Java project, run your Selen

Quick Guide GitHub Actions on Running Selenium Java Tests

AutomationTools, Free Tools

Test Automation Reporting with ReportPortal in .NET Projects

In the next few articles from the Automation Tools Series, I will show you different test automation reporting solutions. Finally, there will be an article comp

Test Automation Reporting with ReportPortal in .NET Projects
Anton Angelov

About the author

Anton Angelov is Managing Director, Co-Founder, and Chief Test Automation Architect at Automate The Planet — a boutique consulting firm specializing in AI-augmented test automation strategy, implementation, and enablement. He is the creator of BELLATRIX, a cross-platform framework for web, mobile, desktop, and API testing, and the author of 8 bestselling books on test automation. A speaker at 60+ international conferences and researcher in AI-driven testing and LLM-based automation, he has been recognized as QA of the Decade and Webit Changemaker 2025.