<?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>Sun, 24 May 2026 22:24:54 +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 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>
