<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Integration Testing - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Integration Testing - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 22 Sep 2026 05:05:21 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/integration-testing/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Automate Your Tests in Express Using Vitest ]]>
                </title>
                <description>
                    <![CDATA[ Thinking through API logic while constantly switching tabs to test application integration can be overwhelming and time-consuming. Well, you can save your time and energy by writing tests for your app ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-automate-your-tests-in-express-using-vitest/</link>
                <guid isPermaLink="false">6a91aa2c85465e213741c4de</guid>
                
                    <category>
                        <![CDATA[ vitest ]]>
                    </category>
                
                    <category>
                        <![CDATA[ unit testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Integration Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mocking ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API mocking ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mocking api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mongoose-mocking ]]>
                    </category>
                
                    <category>
                        <![CDATA[ qa testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Quality Assurance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Supertest ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mockingoose ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ jabo Landry ]]>
                </dc:creator>
                <pubDate>Fri, 28 Aug 2026 15:33:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/098eaca4-3334-4a81-b6f6-ad0b167eee44.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Thinking through API logic while constantly switching tabs to test application integration can be overwhelming and time-consuming.</p>
<p>Well, you can save your time and energy by writing tests for your application that run whenever you add a new feature, all without leaving your IDE during development. This will help you be confident that each feature works as expected.</p>
<p>In this guide, I'll help you build confidence through code: you'll learn how to validate APIs with tests first, then confirm the results in Postman or any other API testing tool.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><strong>Node.js &amp; Express basics:</strong> You should have a working knowledge of Node.js and Express (or a similar library like <code>fastify</code>), including how to build and run a simple API.</p>
</li>
<li><p><strong>Working knowledge of TypeScript:</strong> The code snippets in this guide are written using TypeScript, so you should have a solid understanding of TypeScript basics.</p>
</li>
<li><p><strong>Basic familiarity with MongoDB:</strong> Helpful but not required. The examples in this guide use MongoDB for demonstration purposes, but the underlying logic applies to any database or data layer. Only the tooling differs.</p>
</li>
<li><p><strong>Hands-on API experience:</strong> Prior experience writing at least one backend API with Express will help you follow along more effectively.</p>
</li>
<li><p><strong>Curiosity and motivation:</strong> A willingness to deepen your backend skills by learning how to write and run tests for your APIs.</p>
</li>
<li><p><strong>Environment setup:</strong> Node.js version <strong>20 or higher</strong> installed on your machine.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-testing-concept">Testing Concept</a></p>
</li>
<li><p><a href="#heading-unit-testing">Unit testing</a></p>
</li>
<li><p><a href="#heading-comparison-between-unit-and-integration-tests">Comparison between unit and integration tests</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<p>You can find all the code snippets used in this guide in this <a href="https://github.com/jabo-arnold-landry/testing-lesson">Git repository</a>. Each example has its own branch, and they're combined into a main branch if you want a full example version. Please consider starring the repository if you find it helpful.</p>
<h2 id="heading-key-testing-concepts">Key Testing Concepts</h2>
<p>Testing helps you build confidence in your codebase. You wrie code that tests other code in your application, or code that checks if a function or feature behaves the way that it should.</p>
<p>In this guide, we'll cover two types of testing:</p>
<ul>
<li><p><strong>Unit testing</strong>: This is the most basic type of testing and, in my opinion, the easiest. You test a single piece of code in isolation to see how it behaves.</p>
</li>
<li><p><strong>Integration testing</strong>: This approach is used to test how different parts of the application are integrated to make sure they're working together as intended.</p>
</li>
</ul>
<h3 id="heading-key-testing-terms">Key Testing Terms</h3>
<p>Throughout this guide, I'll be using some technical terms are related to testing which I want to explain up front:</p>
<ul>
<li><p><strong>Mocking</strong>: mocking is a technique used to make a fake implementation of a real function call.</p>
</li>
<li><p><strong>Spying</strong>: spying is a way of inspecting a function to see, for example, if the function is called with a certain type of argument or how many times it's been called.</p>
</li>
<li><p><strong>Assertion</strong>: assertions check to see if the output you're getting matches the expected output.</p>
</li>
</ul>
<p>Alright now that we have that covered, let's go over some basics of testing so you have the best practices down before we start writing tests.</p>
<h3 id="heading-how-to-name-a-test-file">How to Name a Test File</h3>
<p>Make sure your test files follow one of these naming conventions:</p>
<ul>
<li><p><code>filename.test.ts</code></p>
</li>
<li><p><code>filename.spec.ts</code></p>
</li>
<li><p><code>filename.test.js</code></p>
</li>
<li><p><code>filename.spec.js</code></p>
</li>
</ul>
<p>Both the <code>spec</code> and <code>test</code> keyword in a file name makes it possible to run the test file. They also help testing frameworks run the right file.</p>
<p>You can choose either the TypeScript or JavaScript extension on a file based on which language you're using to write the test. For this guide I'm using TypeScript so I'll be using the TypeScript (<code>.ts</code>) test file version.</p>
<h3 id="heading-parts-of-a-testing-file">Parts of a Testing File</h3>
<p>Typically, a test file will have four main parts that you should be familiar with, which are:</p>
<ul>
<li><p><code>describe</code>: Used to describe which test you're going to write.</p>
</li>
<li><p><code>it</code>: Used to specify a condition that a function must pass when tested against.</p>
</li>
<li><p><code>expect</code>: Used to determine what type of results you're expecting when you call a function that's being tested</p>
</li>
<li><p><code>matchers</code>: These are different method available on the <code>expect</code> keyword that we use to evaluate if the function we're testing returns a value that meets the expected data or value.</p>
</li>
</ul>
<p>Example of a test file:</p>
<pre><code class="language-typescript">import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("email validation test suites", () =&gt; {
  it("must define email validation function", () =&gt; {
    expect(validateEmail).toBeDefined();
  });
});
</code></pre>
<p>The above snippet tests if the <code>validateEmail</code> function is defined.</p>
<p>In this test file:</p>
<ul>
<li><p>We use <code>describe</code> to specify the description of the test. <code>describe</code> receives a string description of the test and a function to handle different test cases.</p>
</li>
<li><p>The <code>it</code> keyword specifies and defines a test for a function you're testing, <code>it</code> receives a string describing a specific test description and a callback function to execute and handle test assertion.</p>
</li>
<li><p>Then <code>expect</code> uses the function return type to check if it matches a specific condition through the <code>toBeDefined</code> matcher method.</p>
</li>
</ul>
<h3 id="heading-list-of-matchers">List of Matchers:</h3>
<p>There are many matchers available to you. Below are a few of them:</p>
<ul>
<li><p><code>toBe</code>: compares the passed-in value to see if it matches the function's returned value. It's used on primitive data types like strings, numbers, and so on.</p>
</li>
<li><p><code>toEqual</code>: compares the passed-in value to see if it matches the function's returned value. It's used on non-primitive data types like objects, arrays, and so on.</p>
</li>
<li><p><code>toThrow</code>: used on a function that threw an error to check if the function threw expected error object or instance.</p>
</li>
<li><p><code>toBeCalledWith</code>: used to check if a function is called with a given parameter.</p>
</li>
<li><p><code>toBecalledOnce</code>: used to check if a function is called only once.</p>
</li>
<li><p><code>toBeDefined</code>: used to check if a function is defined.</p>
</li>
<li><p><code>toBeUndefined</code>: used to check if a function returns an undefined value.</p>
</li>
<li><p><code>toBeTruthy</code>: used to check if a function returns a true Boolean value.</p>
</li>
<li><p><code>toBeFalsy</code>: used to check if a function returns a false Boolean value.</p>
</li>
</ul>
<p>These are few of the many matchers out there.</p>
<h3 id="heading-vitest-installation">Vitest Installation</h3>
<p>Now that you know some testing basics, we can get into the actual tests. We'll start by installing <code>vitest</code>, the framework that we'll use to run and write tests for our application.</p>
<p>You can choose your preferred package manager to use to install <code>vitest</code> from the list below:</p>
<pre><code class="language-shell">pnpm add -D vitest # for pnpm package manager
npm install -D vitest # for npm package manager
yarn add -D vitest # for yarn package manager
bun add -D vitest # for bun package manager
</code></pre>
<h2 id="heading-unit-testing">Unit Testing</h2>
<p>A unit test focuses on testing a small piece of code in isolation in your application. A simple example could be if you have a function that adds contact info to a database. For the test, you could check if the email is valid before adding the contact to the database.</p>
<p>Let's start by testing a simple email validation function so you can get comfortable with how unit tests works and how to write one:</p>
<pre><code class="language-typescript">export default function validateEmail(email: string) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (regex.test(email)) {
    return true;
  } else {
    throw new Error("Invalid email format");
  }
}
</code></pre>
<p>The above snippet exports a function that receives an email and then uses regex to validate if the email is valid. It throws an error if the email is invalid.</p>
<h3 id="heading-tests-for-the-validateemail-function">Tests for the <code>validateEmail</code> Function</h3>
<p>Let's start by checking if <code>validateEmail</code> returns true for correct emails:</p>
<pre><code class="language-typescript">import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("email validation test suites", () =&gt; {

  it("returns true for valid email", () =&gt; {
    const sampleEmail = "arnoldjabo@gmail.com";
    expect(validateEmail(sampleEmail)).toBeTruthy();
  });

});
</code></pre>
<p>In the above test, we're creating a variable <code>sampleEmail</code> to be used as a sample email in the <code>validateEmail</code> function. Save this and then run <code>npx vitest</code> in your terminal. You should see a terminal with the results of your test. It should look like the below screenshot:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69c7bcff7cf27065100ae8be/84af7e2b-8344-49c8-a744-35d64ee2a1c0.png" alt="passed tests in vitest terminal" style="display: block;" width="1443" height="492" loading="lazy">

<p>When you run tests, Vitest shows the list of test files you're testing, how many tests were executed, and how many passed and failed tests you have.</p>
<p>Let's create another test that detects an invalid email:</p>
<pre><code class="language-typescript">import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("email validation test suites", () =&gt; {

    it("throws error for invalid email", () =&gt; {
    const sampleEmail = "verymasd.com";
    const invalidEmailResults = () =&gt; validateEmail(sampleEmail);
    expect(invalidEmailResults).toThrow("Invalid email format");
  });

});
</code></pre>
<p>For functions that throw errors, you need to wrap them inside another function to prevent them from stopping the test before the test reaches the assertion or <code>expect</code> section.</p>
<p>In our example above, the <code>validateEmail</code> function is wrapped inside another function which will hold whatever the error <code>validateEmail</code> throws is. It then assigns it to the <code>invalidEmailResults</code> variable. Next we use the <code>toThrow</code> matcher on the <code>expect</code> assertion to match a type of error <code>validateEmail</code> expects to be thrown for an invalid email.</p>
<p>If your run the test, you'll have two passed tests now:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69c7bcff7cf27065100ae8be/521702d7-073b-44e7-90dc-9a30b664b81d.png" alt="Passed test for invalid email that throws an error for an invalid email" style="display: block;" width="1456" height="412" loading="lazy">

<p>If you didn't wrap the <code>validateEmail</code> function inside another function when it throws an error, you'll see something like this when you run the test:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69c7bcff7cf27065100ae8be/693fec20-7f56-42ea-be18-4caa5cba3dac.png" alt="The error message you would receive inside the test terminal if you didn't wrap a function that throws an error inside another function." style="display: block;" width="1440" height="891" loading="lazy">

<p>As you can see, the error fires before reaching the test final assertion. So when you have a function that throws errors, remember to wrap your function inside another function to avoid throwing errors mid-test.</p>
<p><strong>Tip</strong>: When writing unit tests, focus on the expected input and output of a function. You don't have to worry or need to think about the implementation of the function: the input and output are the key elements here.</p>
<h3 id="heading-testing-api-calls">Testing API Calls</h3>
<p>By now you should understand how a unit test works. So next, let's see how you can unit test an API that makes a call to a database.</p>
<pre><code class="language-typescript">import Contacts from "../../schema/contactList";
import { Request, Response } from "express";
import validateEmail from "../../utils/email-validation";

async function addContacts(req: Request, res: Response) {
  const { contactName, phoneNumber, email } = req.body;
  validateEmail(email);

  const contact = await Contacts.create({ contactName, phoneNumber, email });

  return res.status(201).json({ message: `successfully created ${contact.contactName}` });
}

export default addContacts;
</code></pre>
<p>In the code snippet above, we have a function that creates a contact with the database. It also validates if the passed email is valid.</p>
<p>In this example, we're using mongo DB for the database and Mongoose for connecting the codebase with our mongo DB instance.</p>
<p>Unit tests for non-pure functions (that is, functions that are dependent on external services, like making API calls or calling other functions) are tested a bit differently.</p>
<p>For these cases, you create a mock or fake version of the original function that makes a call to the external service and then define its behavior to match the expected return value that you'd have if you used its real version.</p>
<p>We'll start by mocking the implementation of the email validation function. This will help solidify the understanding on how mocking works in unit testing and testing in general.</p>
<p><strong>Note</strong>: mocking the <code>validateEmail</code> function isn't that important because it doesn't make a big difference from using the email validation function directly here. But for learning purposes, we'll mock it to help you understand how it works.</p>
<p>When mocking the modules import, we use the <code>vi.mock</code> function which helps transform the imports of a given module into mocks or fake versions of the real ones.</p>
<pre><code class="language-typescript">vi.mock(filepath,callback);
</code></pre>
<p><code>vi.mock</code> receives two arguments: the file path location of the module you want to mock, and a callback function called a factory function which we'll use to transform the module imports into mocks.</p>
<p>Let's start by mocking the <code>validateEmail</code> path and creating a factory function to transform the module into mocks:</p>
<pre><code class="language-typescript">vi.mock("../utils/email-validation", () =&gt; {
  return { default: vi.fn()};
});
import validateEmail from "../utils/email-validation";
</code></pre>
<p>In the callback function (factory function), we then return an object of the exported module, with a key of <strong>default</strong> and a value of <code>vi.fn</code>. For function mocking we use <code>vi.fn()</code> which automatically replaces the function's return value with <code>undefined</code>.</p>
<p>We use default as a key because the <code>validateEmail</code> function is exported as a default export. If it was a named export, we would have used the actual export name instead of default in the return object.</p>
<pre><code class="language-typescript">vi.mock("../utils/email-validation", () =&gt; {
  return { validateEmail: vi.fn() };
});
import { validateEmail } from "../utils/email-validation";
</code></pre>
<p>Always import your module after the mock module operation to avoid using the real module.</p>
<p>There are methods on <code>vi.fn()</code> that help define the implementation and behaviors of the mocked function. Some of these methods include:</p>
<ul>
<li><p><code>mockReturnValue</code>: Used to define a return value for a mocked function</p>
</li>
<li><p><code>mockRejectsValue</code>: Used for promise-based functions to define the error the function will return.</p>
</li>
<li><p><code>mockResolveValue</code>: Used for promise-based functions to define the data the function will return.</p>
</li>
<li><p><code>mockImplementation</code>: Used to define a new function behavior of a mocked function.</p>
</li>
<li><p><code>mockReturnThis</code>: Used to return the actual instance of a function you're mocking.</p>
</li>
</ul>
<p>These are the methods that you'll likely use most of the time when defining mock implementation and setting mock return value. Just keep in mind that there are many others.</p>
<p>Here, for <code>emailValidate</code>, we'll be using the <code>mockReturnValue</code> and <code>mockImplementation</code> methods.</p>
<p>Let's use <code>mockReturnValue</code> to make the validateEmail function return true by default, assuming the email will be formatted correctly:</p>
<pre><code class="language-typescript">vi.mock("../utils/email-validation", () =&gt; {
  return { default: vi.fn().mockReturnValue(true) };
});
import validateEmail from "../utils/email-validation";
</code></pre>
<p>We define a mock function with <code>vi.fn</code> and then chain on the <code>mockReturnValue(true)</code> to change the mock function default return value (undefined) to true in our case.</p>
<h3 id="heading-defining-the-mocking-implementation">Defining the Mocking Implementation</h3>
<p>You can do a lot with a mocked function, like defining a new implementation for the mocked function that replaces existing logic in the original function.</p>
<p>Let's create a test suite with a fake email that throws an error when you pass the wrong email while creating contacts.</p>
<pre><code class="language-typescript">import { describe, expect, it, vi } from "vitest";
import mockinggoose from "mockingoose";
import Contacts from "../schema/contactList";
import addContacts from "../src/controllers/add-contacts.controller";

import { type Response, type Request } from "express";

vi.mock("../utils/email-validation", () =&gt; {
  return { default: vi.fn().mockReturnValue(true) };
});

import validateEmail from "../utils/email-validation";

const fakeContact = {
  contactName: "arnold",
  phoneNumber: 798600102,
  email: "arnoldjabo@gmail.com",
};

describe("Add contacts to the database", async () =&gt; {
  it("throws error for the wrong email address", async () =&gt; {
    const req = {
      body: { ...fakeContact, email: "fakemail" },
    } as Request;  

    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
    } as any as Response;

    (validateEmail as ReturnType&lt;typeof vi.fn&gt;).mockImplementation(() =&gt; {
      throw new Error("invalid email!");
    });
 
    await expect(addContacts(req, res)).rejects.toThrow();
  });
});
</code></pre>
<p>In the snippet above, we're mocking or creating a fake request object that's cast as request type of <code>express</code>. We do the same with the response object – but the difference here is that with response we're also creating mocks for common methods that you'd use on an Express response (which are status and a <code>json</code> object).</p>
<p>We then turn the return type of the <code>validateEmail</code> function into the vitest mocking function type to avoid TypeScript warnings. Then we use the <code>mockImplementation</code> method to throw a new error inside <code>validateEmail</code>.</p>
<p>The assertion works differently because now we're throwing a promise-based error. We use <code>rejects</code> on the assertion and then chain on another matcher that stimulates which type of error the function will throw.</p>
<p><strong>Tip:</strong> when working with TypeScript, the response object can't be cast like we did on the request object because the response object is much stricter than request. So you'll first need to cast it as any and then cast back to the response object. That way you avoid the TypeScript warning while still keeping the type in play for your test code.</p>
<h3 id="heading-mocking-a-mongoose-model">Mocking a Mongoose Model</h3>
<p>With unit tests, we don't want to save test data to a real database. Instead we can fake the implementation of the service that calls the database service – in our case, we can use the <code>create</code> method from Mongoose. It'll save the record to a Mongo database. We can then define what it should return on success (and it should look identical to what it would return if we were using a real database).</p>
<p>We'll start by installing a library for mocking a Mongoose model called <code>mockingoose</code>:</p>
<pre><code class="language-shell">pnpm add -D mockingoose # for pnpm package manager
npm install -D mockingoose # for npm package manager
yarn add -D mockingoose # for yarn package manager
bun add -D mockingoose # for bun package manager
</code></pre>
<p>After installation we'll create a mock for our contacts model:</p>
<pre><code class="language-typescript">import { describe, expect, it, vi } from "vitest";
import mockinggoose from "mockingoose";
import Contacts from "../schema/contactList";

const fakeContact = {
  contactName: "arnold",
  phoneNumber: 798600102,
  email: "arnoldjabo@gmail.com",
};

describe("Add contacts to the database", async () =&gt; {
  it("successfully create a new contact to the database", async () =&gt; {
    mockinggoose(Contacts).toReturn(fakeContact, "save");
  });

});
</code></pre>
<p>To mock a Mongoose model, we call the <code>mockinggoose()</code> function and pass the model to mock. Then we use the <code>toReturn</code> matcher to describe what it should return, <code>toReturn</code> matcher expects two arguments.</p>
<p>Those arguments are a fake dataset for the model and a Mongo method that we'll use to work with the data. For our example we'll use <code>save</code> because we're creating records in the document.</p>
<h3 id="heading-unit-testing-the-api">Unit Testing the API</h3>
<p>We can start by writing the first test for the add contact API call like this:</p>
<pre><code class="language-typescript">import { describe, expect, it, vi } from "vitest";
import mockinggoose from "mockingoose";
import Contacts from "../schema/contactList";
import addContacts from "../src/controllers/add-contacts.controller";
import { type Response, type Request } from "express";

vi.mock("../utils/email-validation", () =&gt; {
  return { default: vi.fn().mockReturnValue(true) };
});

import validateEmail from "../utils/email-validation";

const fakeContact = {
  contactName: "arnold",
  phoneNumber: 798600102,
  email: "arnoldjabo@gmail.com",
};

describe("Add contacts to the database", async () =&gt; {
  it("successfully create a new contact to the database", async () =&gt; {
    mockinggoose(Contacts).toReturn(fakeContact, "save");

    const req = {
      body: fakeContact,
    } as Request;

    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
    } as any as Response;

    await addContacts(req, res);
    expect(res.status).toHaveBeenCalledWith(201);
    expect(res.json).toHaveBeenCalledWith({
      message: `successfully created ${fakeContact.contactName}`,
    });
  });
 });
</code></pre>
<p>For this test assertion, we're using different matchers called <strong>spies</strong>. These are used on a function to inspect how many times it's been called or which parameters were used to call it (and so on).</p>
<p>Here we're expecting the status function of response to be called with a status of 201 as its argument. Then the JSON object is called with a message argument that we're using to send out the response.</p>
<h2 id="heading-integration-tests">Integration Tests</h2>
<p>Integration tests test the communication and integration of different parts of an application. For example, they might check if your database integrates well with the function that makes the API call to the database.</p>
<p>Unlike unit tests (where we don't need to have our test making API calls), with integration tests we're testing if parts of the application integrate together and works as expected. We don't need to mock anything, because we want to make sure that we're successfully sending a request to the backend and connecting to the database.</p>
<h3 id="heading-creating-integration-test-data-storage">Creating Integration Test Data Storage</h3>
<p>When you're running integration tests, there are two ways to create a testing environment that acts as a database. They include:</p>
<ul>
<li><p>Creating a duplicate schema of your real database and using the copy as a testing database environment. Whenever you're running tests, you point your database connectivity to the test DB.</p>
</li>
<li><p>Creating in-memory database storage. This approach doesn't require you to have two separate schemas (one for testing and another for production). Instead you construct the same schema shape in your codebase memory and use it as your testing environment.</p>
</li>
</ul>
<p>Using the first approach is complicated because you have to set up and configure which database to use for which environment. But for the second approach, you can just set up the right schema structure as the original schema and use it for testing without needing to configure it in the database and remove it after use.</p>
<p>For Mongo DB there's a package that simplifies the in-memory storage option for us called <code>mongodb-memory-server</code>. It deletes all the data that was used for testing after the tests have finished running.</p>
<h3 id="heading-how-to-set-up-the-environment-for-the-integration-tests">How to Set Up the Environment for the Integration Tests</h3>
<p>You'll need to install:</p>
<ul>
<li><p><code>supertest</code>: a package that helps you make API calls/requests and returns back the response when testing.</p>
</li>
<li><p><code>mongodb-memory-server</code>: a package that makes in memory database storage for testing data.</p>
</li>
</ul>
<pre><code class="language-shell"># command for pnpm package manager
pnpm add -D mongodb-memory-server supertest @types/supertest

# command for npm package manager
npm install --save-dev mongodb-memory-server supertest @types/supertest

# command for yarn package manager
yarn add --dev mongodb-memory-server supertest @types/supertest

# command for bun package manager
bun add -d mongodb-memory-server supertest @types/supertest
</code></pre>
<p>Before moving on, we need to change the setup of our server entry file.</p>
<p>If you've been using Express with Node or any other framework, you may be familiar with the following type of setup for the server entry file where everything is added into a single file:</p>
<pre><code class="language-typescript">import express from "express";
import { loadEnvFile } from "node:process";
import connectToDB from "../config/dbConfig";
import addContacts from "./controllers/add-contacts.controller";

const app = express();
loadEnvFile();
async function dbConnection() {
  await connectToDB();
}
dbConnection();

app.use(express.json());
app.post("/add-contacts", addContacts);
app.listen(5000, () =&gt; console.log("the server successfully connected"));
</code></pre>
<p>This setup works fine and it's valid in certain cases. But when working with integration tests, it can be problematic. This is because in integration tests, we'll need an instance of Express to use when making the request. If we export the <code>app</code> variable here inside the main file when we make a request while testing, the production DB connection will conflict with the testing DB connection. This'll cause the tests to stop working.</p>
<p>The solution here is create another file, define an Express instance, and export it. Then we'll use the exported Express instance in the server to start a server. The setup looks like this:</p>
<p><code>app.ts</code></p>
<pre><code class="language-typescript">import express from "express";
import addContacts from "./controllers/add-contacts.controller";

const app = express()

app.use(express.json())
app.post("/add-contacts", addContacts);

export default app;
</code></pre>
<p>Then the main file <code>server.ts</code> or <code>main.ts</code> uses the <code>app</code> variable like this:</p>
<pre><code class="language-typescript">import { loadEnvFile } from "node:process";
import connectToDB from "../config/dbConfig";
import app from "./app";

loadEnvFile();

async function bootsrap() {
  await connectToDB();
  app.listen(5000, () =&gt; console.log("the server successfully connected"));
}
bootsrap();
</code></pre>
<p>We're importing the Express instance from the <code>app</code> file and then using the <code>bootstrap()</code> function to set up the database and start the server. With this in place, we can start writing integration tests for the <code>addContact</code> module.</p>
<h3 id="heading-how-to-write-the-integration-tests">How to Write the Integration Tests</h3>
<p><strong>Tip</strong>: With integration test(s) you can name your file like <code>filename.integration.test.ts</code> this is the most commonly used naming convention for integration tests, but it is not mandatory it just a naming convention.</p>
<p>You first need to set up the database testing data storage using the <code>mongoose</code> and <code>mongdb-memory-server</code> packages and the Express instance for making requests.</p>
<pre><code class="language-typescript">import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import app from "../src/app";


describe("intergration test setup for add contact api", () =&gt; {
  let mongoServer: MongoMemoryServer;
  let server: any;

  beforeAll(async () =&gt; {
    mongoServer = await MongoMemoryServer.create();
    const uri = mongoServer.getUri();
    await mongoose.connect(uri);
    server = app.listen(0);
  });

  afterAll(async () =&gt; {
    mongoServer.stop();
    mongoose.disconnect();
    server.close();
  });
});
</code></pre>
<p>The <code>beforeAll</code> and <code>afterAll</code> functions are <code>vitest</code> functions. <code>beforeAll</code> runs before any test starts executing and <code>afterAll</code> will run after all tests are done executing.</p>
<p>In the test, we set up the database and Express instance before any test runs.</p>
<p>First, we created the <code>mongoServer</code> variable. Then, inside the <code>beforeAll</code> block, we initialize it with <code>MongoMemoryServer.create</code> to create an in-memory database for testing data storage. We get the connection string using the <code>uri</code> variable using the <code>getUri</code> method. Finally we use Mongoose to connect to the generated in-memory connection string.</p>
<p>The server variable is assigned to the Express instance listening to port 0, but you can use any port number of your choice – it's just for demonstration purposes. This creates an Express instance for our testing environment.</p>
<p>In <code>afterAll</code>, after all tests have finished executing, we close the server and in-memory DB and then also disconnect our Mongoose instance.</p>
<p>Within the same <code>describe</code> block, we then add the test description and assertion (same as we did in unit testing):</p>
<pre><code class="language-typescript">import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import app from "../src/app";
import request from "supertest";
import Contacts from "../schema/contactList";

describe("intergration test for add contact api", () =&gt; {
  /*
    Here we do server setup and database in memory setup that was discussed,
    in the previous snippets for setting up integration data storage testing environment 
   */
  const fakeContact = {
    contactName: "arnold",
    phoneNumber: 798600102,
    email: "arnoldjabo@gmail.com",
  };

  describe("POST /add-contacts", () =&gt; {
    it("creates a new record to the database", async () =&gt; {
      const response = await request(app)
        .post("/add-contacts")
        .send(fakeContact);

       const contactList = await Contacts.findOne({
        email: "arnoldjabo@gmail.com",
      })!;
      expect(contactList?.email).toBe("arnoldjabo@gmail.com");
      console.log(contactList);

      expect(response.status).toBe(201);
      expect(response.body).toEqual({
        message: `successfully created ${fakeContact.contactName}`,
      });
    });
  });
});
</code></pre>
<p>Here in the test file, we're describing the test as a post method test for the add-contact endpoint. Then we test if it adds data to the database.</p>
<p>Within the <code>it</code> body, we use <code>request</code> from <code>supertest</code> to make a request to the server we've created. We also chain on an HTTP method with the endpoint we want to test.</p>
<p>For methods that send data to the backend like POST, PATCH, or PUT, we use the <code>send</code>() method on <code>request</code> to add an object of the data that we're sending.</p>
<p>We'll use the response to assert what the response could look like. For example, we're expecting the server to give a status code of 201 on successful data entry and a JSON object with a message property that confirms that it has added a contact.</p>
<p>We're using an assertion to check if the response's status matches what we expect, as well as if the response body matches the expected message we should be getting.</p>
<p>To test if the data are really being added to the database, I've added <code>contactList</code> to get the contact we just added by finding it by email. Then we log the <code>contactList</code> to the console to show how in-memory works. It's pretty much the same as a real Mongo DB instance. If we were to run the tests, we would have something that looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69c7bcff7cf27065100ae8be/e20082ae-0d06-48e2-b40b-8c66846c7ed4.png" alt="Out-put for integration test with the console log showing how that stored using in memory database looks like when printed to the screen." style="display: block;" width="1460" height="847" loading="lazy">

<p>You can see from the console the in-memory stores and retrieves data as a regular Mongo database does.</p>
<h2 id="heading-when-to-use-unit-vs-integration-tests">When to Use Unit vs Integration Tests</h2>
<p>So when do you use each type of test?</p>
<p>Use unit tests when you have pure functions like the email validation example we had earlier.</p>
<p>And use Integration tests for functions that makes external API calls that are dependent on external service like database calls to avoid mocking every function that you're importing.</p>
<h2 id="heading-summary">Summary</h2>
<p>This guide explains how two types of testing work: unit tests and integration tests.</p>
<p>Unit tests are code that tests specific pieces of your codebase in isolation, and are best for pure functions. Integration test are code that tests successful integration and communication between parts of your application, and they're best for non-pure functions.</p>
<p>If you found the article helpful, you can <a href="https://buymeacoffee.com/jabo1200">buy me coffee</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use TestContainers in .Net ]]>
                </title>
                <description>
                    <![CDATA[ At some point in your development lifecycle, you will need to test that your system can integrate with another system, whether it be another API, a database, or caching service, for example. This can be a laborious task of spinning up other servers h... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-testcontainers-in-net/</link>
                <guid isPermaLink="false">67e2cbfdaa97659cd53cf39f</guid>
                
                    <category>
                        <![CDATA[ C# ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testcontainers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Integration Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Grant Riordan ]]>
                </dc:creator>
                <pubDate>Tue, 25 Mar 2025 15:30:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742773343798/44c64acc-3862-4325-af21-6b7de417d300.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>At some point in your development lifecycle, you will need to test that your system can integrate with another system, whether it be another API, a database, or caching service, for example. This can be a laborious task of spinning up other servers hosting the 3rd party API replica, or permanently hosting a SQL database seeded with test data.</p>
<p>In this article, I’ll teach you how to use the TestContainers library to make running integration tests much easier and more manageable.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-testcontainers">What Is TestContainers?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-does-it-all-work">How Does It All Work?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-your-first-test">How to Set Up Your First Test</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-behaviors-of-iasynclifetime-in-a-test-class">Key Behaviors of IAsyncLifetime in a Test Class</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-improve-performance">How to Improve Performance</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-explanation-of-differences">Explanation of Differences</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-share-your-container-across-multiple-test-classes">How to Share Your Container Across Multiple Test Classes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary-of-approaches">Summary of Approaches:</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-multiple-containers">How to Create Multiple Containers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-make-your-setup-easier-with-custom-images">How to Make Your Setup Easier With Custom Images</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Understanding of Docker</p>
</li>
<li><p>Understanding of xUnit and testing</p>
</li>
<li><p>Installation of the following packages:</p>
<ul>
<li><p><code>TestContainers</code></p>
</li>
<li><p><code>TestContainers.MsSql</code></p>
</li>
<li><p>xUnit</p>
</li>
<li><p>&gt;= .Net 8</p>
</li>
<li><p><code>FluentAssertions</code></p>
</li>
<li><p><code>Microsoft.Data.SqlClient</code></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-what-is-testcontainers">What Is TestContainers?</h2>
<p><a target="_blank" href="https://testcontainers.com">TestContainers</a> is an open source library that provides you with easily disposable container instances for things like database hosting, message brokers, browsers and more – basically anything that can run in a Docker container.</p>
<p>It removes the necessity to maintain hosted environments for testing in the cloud or on local machines. As long as the user’s machine and CI/CD host supports Docker, the testContainer tests can easily be run.</p>
<h2 id="heading-how-does-it-all-work">How Does It All Work?</h2>
<p>You define the image you’re wanting to utilise, and specify a configuration.</p>
<p>The TestContainer library spins up a Docker Container with the configured image.</p>
<h3 id="heading-provides-connection-details"><strong>Provides Connection Details</strong></h3>
<p>After starting the container, TestContainers exposes connection strings (for example, a database connection URL), so your tests can use the real service, rather than having to configure this yourself.</p>
<h3 id="heading-cleans-up-automatically"><strong>Cleans Up Automatically</strong></h3>
<p>When the test finishes, TestContainers removes the container automatically, ensuring no leftover resources. This is one of the best things about using TestContainers: all the creation, tear down, and container setup is handled within the library itself, making it perfect for use within delivery pipelines.</p>
<h2 id="heading-how-to-set-up-your-first-test">How to Set Up Your First Test</h2>
<p>For the purpose of this tutorial, we’re going to keep things simple, and only use a <code>MS Sql Server</code> image.</p>
<p>The first thing we’re going to do is configure our Microsoft SQL Server Docker container via the TestContainer fluid API.</p>
<p>Create your test class like below:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">IntegrationTests</span>: <span class="hljs-title">IAsyncLifetime</span> 
{
    <span class="hljs-keyword">private</span> MsSqlContainer _container;
    <span class="hljs-keyword">private</span> FakeLogger _<span class="hljs-function">logger

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeAsync</span>(<span class="hljs-params"></span>)</span>
    {
           _container = <span class="hljs-keyword">new</span> MsSqlBuilder()
                .WithImage(<span class="hljs-string">"mcr.microsoft.com/mssql/server:2022-latest"</span>)
                .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
                .WithPortBinding(<span class="hljs-number">1443</span>)
                .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
                .Build();

            _logger = <span class="hljs-keyword">new</span> FakeLogger();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">DisposeAsync</span>(<span class="hljs-params"></span>)</span> =&gt; <span class="hljs-keyword">await</span> _container.DisposeAsync();
}
</code></pre>
<p>Here we’re using xUnit’s <code>IAsyncLifetime</code> interface. It’s an interface in xUnit that provides a way to handle async setup and teardown for test classes. It's useful when you need to initialise and clean up resources asynchronously. We’re using the <code>InitializeAsync()</code> to setup and define our Microsoft SQL Database container as well as starting the container, then using the <code>DisposeAsync()</code> method to stop and dispose of our container.</p>
<h3 id="heading-explanation-of-builder-methods">Explanation of Builder Methods</h3>
<ul>
<li><p><code>WithImage()</code>: this allows us to specify the image we want Docker to pull down and run. We’ve opted for the latest version of SQL Server 2022.</p>
</li>
<li><p><code>WithPassword()</code>: This allows us to specify the password for the database (when creating most databases, a password is normally required).</p>
</li>
<li><p><code>WithPortBinding()</code>: This allows us to specify both the hosting port number on your machine, as well as the container port number</p>
</li>
<li><p><code>WithWaitStrategy()</code>: Here we can specify a wait strategy, which informs our container to wait for a condition before the container is ready to use. This is important because some services (like databases or APIs) take time to fully start up.</p>
</li>
<li><p><code>Build()</code>" This is the command that builds the test container based on the configuration. This <strong>does not</strong> run or start the container – you can do this using the <code>container.StartAsync()</code> method as mentioned previously.</p>
</li>
</ul>
<h4 id="heading-why-is-withwaitstrategy-needed"><strong>Why Is</strong> <code>WithWaitStrategy()</code> Needed?</h4>
<p>By default, TestContainers assumes the container is ready as soon as it starts running. But some services might:</p>
<ul>
<li><p>Take time to initialize.</p>
</li>
<li><p>Require a specific log message before they are ready.</p>
</li>
<li><p>Need a port to be accessible before you can connect.</p>
</li>
</ul>
<p>Using <code>WithWaitStrategy()</code>, you can customise how TestContainers waits before considering the container "ready."</p>
<h3 id="heading-adding-the-test">Adding the Test</h3>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">IntegrationTests</span>: <span class="hljs-title">IAsyncLifetime</span> 
{
    <span class="hljs-keyword">private</span> MsSqlContainer _container;
    <span class="hljs-keyword">private</span> FakeLoger _logger;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeAsync</span>(<span class="hljs-params"></span>)</span>
    {
           _container = <span class="hljs-keyword">new</span> MsSqlBuilder()
                .WithImage(<span class="hljs-string">"mcr.microsoft.com/mssql/server:2022-latest"</span>)
                .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
                .WithPortBinding(<span class="hljs-number">1443</span>)
                .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
                .Build();

            <span class="hljs-keyword">await</span> _container.StartAsync();
            _logger = <span class="hljs-keyword">new</span> FakeLogger();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">DisposeAsync</span>(<span class="hljs-params"></span>)</span> =&gt; <span class="hljs-keyword">await</span> _container.DisposeAsync();

    [<span class="hljs-meta">Fact</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Test_Database_Connection</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> connectionString = _container.GetConnectionString();
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> conn = <span class="hljs-keyword">new</span> SqlConnection(connectionString);
        <span class="hljs-keyword">await</span> conn.OpenAsync();

        Assert.True(conn.State == System.Data.ConnectionState.Open);
    }
}
</code></pre>
<p>The above test, although it’s simple, illustrates how easy it is to spin up a container and create a simple test. The above test will work, but it can lead to low performing tests and high usage of machine resource when not used correctly. Let me explain:</p>
<p>Using <code>IAsyncLifetime</code> is necessary, as we’re calling async setup methods (<code>StartAsync</code>), for example. But the <code>InitializeAsync() / DisposeAsync()</code> methods when situated in a test class are run before and after every test (<code>Fact</code> in xUnit).</p>
<p>This means that every time a test begins, it is:</p>
<ul>
<li><p>creating a brand new Docker container,</p>
</li>
<li><p>pulling the MS Sql image,</p>
</li>
<li><p>creating the DB,</p>
</li>
<li><p>running the tests, and</p>
</li>
<li><p>tearing down the container.</p>
</li>
</ul>
<p>You can test this by copying and pasting the above <code>Test_Database_Connection()</code> test multiple times, adding a number to each duplicate test (to keep the compiler happy), and opening Docker Desktop. Running all the tests, you will see a new container (with a different name) being created for each test run.</p>
<p>Now, this can be acceptable if you have a limited number of tests in your test class. But it can have negative outcomes on test classes with a larger number of tests, meaning test maintenance and planning is key. It’s useful, though, when you want to make sure that the database is in a completely clean state before each test, ensuring no data contamination from other tests running.</p>
<h2 id="heading-key-behaviors-of-iasynclifetime-in-a-test-class"><strong>Key Behaviors of</strong> <code>IAsyncLifetime</code> in a Test Class</h2>
<p>When your test class implements <code>IAsyncLifetime</code>, xUnit's default behaviour is:</p>
<p>1. Creates a new instance of the test class for each test method.<br>2. Calls <code>InitializeAsync()</code> before each test.<br>3. Calls <code>DisposeAsync()</code> after each test.</p>
<h3 id="heading-what-does-this-mean-for-testcontainers"><strong>What Does This Mean for TestContainers?</strong></h3>
<ul>
<li><p>In our case, since <code>InitializeAsync()</code> sets up a new container, a new container is created for each test.</p>
</li>
<li><p><code>DisposeAsync()</code> stops the container after each test finishes.</p>
</li>
<li><p>Ensures a completely fresh database state for every test, avoiding data contamination.</p>
</li>
<li><p>Is slow and resource-intensive, especially if you have many test methods.</p>
</li>
</ul>
<p>A more visual look on a test class could look like this:</p>
<p>🟢 InitializeAsync() -&gt; New Container Created (For Test_1)</p>
<p>🧪 Running Test_1</p>
<p><strong>🛑</strong> DisposeAsync() -&gt; Container Stopped (After Test_1)</p>
<p>🟢 InitializeAsync() -&gt; New Container Created (For Test_2)</p>
<p>🧪 Running Test_2</p>
<p><strong>🛑</strong> DisposeAsync() -&gt; Container Stopped (After Test_2)</p>
<h3 id="heading-when-is-this-useful"><strong>When Is This Useful?</strong></h3>
<ul>
<li><p>You need a completely fresh database state or container for each test.</p>
</li>
<li><p>Avoids test data contamination.</p>
</li>
<li><p>Each test starts from a clean slate.</p>
</li>
</ul>
<h3 id="heading-when-is-this-a-problem"><strong>When Is This a Problem?</strong></h3>
<ul>
<li><p>It results in slow execution – a new container is started for every test.</p>
</li>
<li><p>It’s resource-heavy – multiple containers run sequentially.</p>
</li>
<li><p>And it’s not scalable – hundreds of tests will take a long time to complete.</p>
</li>
</ul>
<h2 id="heading-how-to-improve-performance">How to Improve Performance</h2>
<p>Ok, so we’ve seen how to create containers once per test, and explored scenarios where this would be useful, but what if performance and cost are a concern?</p>
<p>Here we can combine <code>IClassFixture</code> and <code>IAsyncLiftetime</code> to achieve a <em>Once per test class</em> approach, where we create one container and one database, and its lifecycle is the full length of the test class (that is, all tests run against the same DB).</p>
<h3 id="heading-how-to-write-this">How to Write This</h3>
<p>We can utilise a TestFixture class which inherits the IAsyncLifetime interface, exposing the <code>InitializeAsync()</code> and <code>DisposeAsync()</code> methods as before.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> DotNet.Testcontainers.Builders;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Logging.Testing;
<span class="hljs-keyword">using</span> Testcontainers.MsSql;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">IntegrationTests</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TestClassFixture</span> : <span class="hljs-title">IAsyncLifetime</span>
{
    <span class="hljs-keyword">public</span> MsSqlContainer Container { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">private</span> FakeLogger _logger;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeAsync</span>(<span class="hljs-params"></span>)</span>
    {
        Container = <span class="hljs-keyword">new</span> MsSqlBuilder()
            .WithImage(<span class="hljs-string">"mcr.microsoft.com/mssql/server:2022-latest"</span>)
            .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
            .WithPortBinding(<span class="hljs-number">1443</span>)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
            .Build();

        _logger = <span class="hljs-keyword">new</span> FakeLogger();
        <span class="hljs-keyword">await</span> Container.StartAsync();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">DisposeAsync</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">await</span> Container.DisposeAsync();
    }
}
</code></pre>
<p>Using xUnit’s <code>IClassFixture</code> interface, we can pass our <code>TestClassFixture</code> and have our test class inherit from this. A test fixture is only run once per test class, making it perfect for our scenario.</p>
<pre><code class="lang-csharp">
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">IntegrationFixtureTests</span> : <span class="hljs-title">IClassFixture</span>&lt;<span class="hljs-title">TestClassFixture</span>&gt;
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _connectionString;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IntegrationFixtureTests</span>(<span class="hljs-params">TestClassFixture testClassFixture</span>)</span>
    {
        _connectionString = testClassFixture.Container.GetConnectionString();

        <span class="hljs-comment">// other test class specific setup goes here</span>
    }

    [<span class="hljs-meta">Fact</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Test_Database_Connection</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> conn = <span class="hljs-keyword">new</span> SqlConnection(_connectionString);
        <span class="hljs-keyword">await</span> conn.OpenAsync();

        Assert.True(conn.State == System.Data.ConnectionState.Open);
    }
}
</code></pre>
<p>We now have a much cleaner test class, and all our container logic is handled by the <code>IClassFixture</code> instead. Should you need to add test class specific code, for example seeding the database prior to running, or the mocking of any resources, you can place this code within the constructor.</p>
<h2 id="heading-explanation-of-differences">Explanation of Differences</h2>
<p>We set our <code>Container</code> property as public, rather than private so that our test class can access the container. The test fixture is injected by xUnit's own internal dependency injection mechanics when you use <code>IClassFixture&lt;T&gt;</code>.</p>
<p>xUnit automatically creates an instance of the fixture class and passes it into the test class constructor.</p>
<p>The container is started within the <code>InitializeAsync()</code> method on the <strong>TestFixture</strong> now, rather than the test class, meaning it only gets started once and is readily available for all the tests. This improves performance and test speeds (no more waiting for each container to spin up before each test).</p>
<p>The test flow would look something more like this now:</p>
<p>🟢 InitializeAsync() → Container Created → Container Started</p>
<p>🧪 Running Test_1</p>
<p>🧪 Running Test_2</p>
<p><strong>🛑</strong> DisposeAsync() -&gt; Container Stopped → Container Disposed of</p>
<h3 id="heading-advantages-and-disadvantages">Advantages and Disadvantages</h3>
<h4 id="heading-faster-execution">✅ <strong>Faster Execution</strong></h4>
<p>Significantly reduces setup/teardown overhead, especially when using slow-starting services like databases.</p>
<h4 id="heading-lower-resource-usage">✅ <strong>Lower Resource Usage</strong></h4>
<p>Running a container once per test class consumes far fewer system resources compared to one container per test. This is especially beneficial when running integration tests in CI/CD pipelines where resource usage needs to be optimised to keep costs low.</p>
<h4 id="heading-more-realistic-testing">✅ <strong>More Realistic Testing</strong></h4>
<p>In real-world scenarios, applications don’t restart their databases between API calls, so why should your integration tests?</p>
<h4 id="heading-data-contamination">❌ <strong>Data Contamination</strong></h4>
<p>Effective test data management is essential for maintaining reliable tests. If test data is not properly isolated, it can lead to unintended interference between tests.</p>
<p>For example, a test that creates a new record might introduce unexpected data, causing a retrieval test to fail if it runs afterward. This type of data contamination is a common issue when all tests in a test class share the same database setup. But,with careful test design—such as proper data isolation, cleanup strategies, or using transactional rollbacks—these issues can be mitigated or entirely avoided.</p>
<h4 id="heading-more-care-needs-to-be-taken-around-indempotency">❌ <strong>More Care Needs To Be Taken Around Indempotency</strong></h4>
<p>“Indempotency” refers to the ability to run any test on its own in any order. If the test class is accessing data from the same areas, the assertions may fail when ran in certain orders than others. For example:</p>
<ul>
<li><p>Test_1 inserts a record.</p>
</li>
<li><p>Test_2 assumes the table is empty and asserts <code>QueryByName()</code> should return 1 record</p>
</li>
<li><p>Test_2 fails because Test_1 has already inserted its own record</p>
</li>
</ul>
<h2 id="heading-how-to-share-your-container-across-multiple-test-classes">How to Share Your Container Across Multiple Test Classes</h2>
<p>So we’ve covered a container per test and a container per test class. But what about sharing a container for multiple test classes? Well, it’s as simple as using the <code>ICollectionFixture</code> interface instead of <code>IClassFixture</code>, and it can be used like so:</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">CollectionDefinition(<span class="hljs-meta-string">"Database collection"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DatabaseCollection</span> : <span class="hljs-title">ICollectionFixture</span>&lt;<span class="hljs-title">TestClassFixture</span>&gt;
{
    <span class="hljs-comment">// This class has no code, </span>
    <span class="hljs-comment">// it’s just used to apply the [Collection] attribute to test classes.</span>
}
</code></pre>
<p>The <code>ICollectionFixture&lt;T&gt;</code> mechanism in xUnit automatically ties the fixture instance to all test classes marked with the <code>[Collection("Collection Name")]</code> attribute, for example:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> IntegrationTests;
<span class="hljs-keyword">using</span> Microsoft.Data.SqlClient;

[<span class="hljs-meta">Collection(<span class="hljs-meta-string">"Database collection"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">IntegrationFixtureTests</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _connectionString;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IntegrationFixtureTests</span>(<span class="hljs-params">TestClassFixture testClassFixture</span>)</span>
    {
        _connectionString = testClassFixture.Container.GetConnectionString();
    }

    [<span class="hljs-meta">Fact</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Test_Database_Connection</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> conn = <span class="hljs-keyword">new</span> SqlConnection(_connectionString);
        <span class="hljs-keyword">await</span> conn.OpenAsync();

        Assert.True(conn.State == System.Data.ConnectionState.Open);
    }
}

[<span class="hljs-meta">Collection(<span class="hljs-meta-string">"Database collection"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AnotherIntegrationTest</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _connectionString;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">AnotherIntegrationTest</span>(<span class="hljs-params">TestClassFixture testClassFixture</span>)</span>
    {
        _connectionString = testClassFixture.Container.GetConnectionString();
    }

    [<span class="hljs-meta">Fact</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Another_Database_Test</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> conn = <span class="hljs-keyword">new</span> SqlConnection(_connectionString);
        <span class="hljs-keyword">await</span> conn.OpenAsync();

        Assert.True(conn.State == System.Data.ConnectionState.Open);
    }
}
</code></pre>
<p>Now you can group your integration tests, whether it be all read tests or all write tests – making your tests much more maintainable.</p>
<h2 id="heading-summary-of-approaches">Summary of Approaches:</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Approach</strong></td><td><strong>Container Creation</strong></td><td><strong>Best For</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>IAsyncLifetime</code> inside the test class</td><td><strong>One per test</strong></td><td>When a fresh DB state per test is needed, avoiding test contamination</td></tr>
<tr>
<td><code>IClassFixture&lt;T&gt;</code> with <code>IAsyncLifetime</code></td><td><strong>One per test class</strong></td><td>Faster execution, sharing DB instance across tests in a class</td></tr>
<tr>
<td><code>ICollectionFixture&lt;T&gt;</code> with <code>IAsyncLifetime</code></td><td><strong>One per multiple test classes</strong></td><td>Sharing a DB instance across different test classes</td></tr>
</tbody>
</table>
</div><h2 id="heading-how-to-create-multiple-containers">How to Create Multiple Containers</h2>
<p>Yes, you can create multiple containers which can host different images, making it perfect for when you have multiple systems you need to integrate with – for example Microsoft SQL Server and a Redis instance.</p>
<p>You can do this by calling the constructor of the relevant TestContainer package like below:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TestContainersFixture</span> : <span class="hljs-title">IAsyncLifetime</span>
{
    <span class="hljs-keyword">public</span> MsSqlContainer SqlContainer { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> RedisContainer RedisContainer { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeAsync</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-comment">// SQL Server Container</span>
        SqlContainer = <span class="hljs-keyword">new</span> MsSqlBuilder()
            .WithImage(<span class="hljs-string">"mcr.microsoft.com/mssql/server:2022-latest"</span>)
            .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
            .WithPortBinding(<span class="hljs-number">1433</span>)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
            .Build();

        <span class="hljs-comment">// Redis Container</span>
        RedisContainer = <span class="hljs-keyword">new</span> RedisContainerBuilder()
            .WithImage(<span class="hljs-string">"redis:latest"</span>)
            .WithPortBinding(<span class="hljs-number">6379</span>)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">6379</span>))
            .Build();

        <span class="hljs-keyword">await</span> Task.WhenAll(SqlContainer.StartAsync(), RedisContainer.StartAsync());
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">DisposeAsync</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">await</span> Task.WhenAll(SqlContainer.DisposeAsync(), RedisContainer.DisposeAsync());
    }
}
</code></pre>
<p>And just like that, we have a SQL Server and a Redis instance ready to integrate test against.</p>
<h2 id="heading-how-to-make-your-setup-easier-with-custom-images">How to Make Your Setup Easier With Custom Images</h2>
<p>To make testing easier, and leverage the power of Docker and TestContainers, here’s a great tip. TestContainers fully supports using custom images, including pre-configured ones with seeded databases. Instead of defining everything in the test setup, you can build and use a custom Docker image that already contains the required schema and test data.</p>
<p>When creating your own custom package to use, you can:</p>
<ol>
<li>Upload your custom image to DockerHub and reference from there:</li>
</ol>
<pre><code class="lang-csharp"> SqlContainer = <span class="hljs-keyword">new</span> MsSqlBuilder()
            .WithImage(<span class="hljs-string">"your-dockerhub-username/custom-sql-image"</span>) 
            .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
            .WithPortBinding(<span class="hljs-number">1433</span>)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
            .Build();
</code></pre>
<ol start="2">
<li>Build your Docker image locally - f you're using a local image in TestContainers, you can simply reference the image name (e.g., <code>my-custom-sql-image</code>) in your code. TestContainers will first check your local Docker Desktop for the image before attempting to pull it from a registry like Docker Hub.</li>
</ol>
<pre><code class="lang-csharp">SqlContainer = <span class="hljs-keyword">new</span> MsSqlBuilder()
    .WithImage(<span class="hljs-string">"custom-sql-image"</span>) <span class="hljs-comment">// Reference your local image</span>
    .WithPassword(<span class="hljs-string">"P@ssw0rd123"</span>)
    .WithPortBinding(<span class="hljs-number">1433</span>)
    .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(<span class="hljs-number">1433</span>))
    .Build();
</code></pre>
<p>Having a pre-built image can speed up your tests especially in CI/CD pipelines, not to mention make them more readable by removing the seeding code.</p>
<p>To access your custom image in a CI/CD pipeline, you can upload it to DockerHub or GitHub Container Registry (GHCR) and access it from your tests. Build your DockerFile and push it to either system before accessing it in your tests.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Using TestContainers in .NET is a game-changer for integration testing. It’s a lightweight and automated way to manage external dependencies like databases, caching systems, and more. By using test containers in a test class, TestFixture, or ICollectionFixture, you can create cleaner, more reliable tests with isolated environments.</p>
<p>TestContainers can also save you money by eliminating the need for dedicated testing environments with long-lived dependencies. You can create and destroy them on the fly, or even integrate them into your CI/CD pipelines, especially in GitHub where Docker can be easily used.</p>
<p>As always I hope you’ve found this article helpful, and if you have any questions don’t hesitate to reach out on X / Twitter - <a target="_blank" href="https://x.com/grantdotdev">@grantdotdev</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
