xUnit 使用 FluentDocker 透過 docker-compose.yml 建立 MongoDB 的 Docker Image 和 Docker Container

前一篇「xUnit 完全使用 FluentDocker 建立 MongoDB 的 Docker Image 和 Docker Container」裡用了兩個步驟分別建立 docker-image 和 docker-container,而建立 docker-image  是透過執行 docker-compose.yml  的方式。

如果不想要這麼麻煩地分成兩步驟,而是想要執行一次 docker-compose 就完成 docker-image 和 docker-container 的建立,這篇文章就來簡單說明如何進行。

之前有說過為什麼只有透過 FluentDocker 執行 docker-compose 建立 docker-image  的理由,因為我希望可以在建立 docker-container 時可以動態指定 port,如果是完全都使用 docker-compose.yml 的設定,那麼很多設定內容都會固定寫死的,就無法滿足我想要動態指定 docker-container port 的需求。另外一個點則是我還是比較喜歡在 xUnit 裡使用非同步的方法來執行,但是 FluentDocker 目前在我所使用的功能裡都還沒有提供非同步方法,於是我就混合著 FluentDocker  與 Testcontainers 分別負責建立 docker-image、docker-container 的工作。

不過腦筋動得快的人應該就可以想到,既然是想要在建立 docker-container 時可以去指定 port 的話,那麼只需要在執行 docker-compose  前去修改 docker-compose.yml 內容,將程式執行時隨機產生的 port 覆寫到 docker-compose.yml  不就好了…

沒錯,就是想要這麼做。說穿了也沒有什麼高深刁鑽的技巧,就只是有沒有想到與要不要這麼做而已。

docker-compose.yml 檔案

先準備好 docker-compose.yml 文件,把要執行的 container 設定都寫好,其中使用到的 mongo_Dockerfile 檔案內容可以查看上一篇文章

 其中 ports  的內容 , 就是等一下要在執行時去修改的地方。

 

修改後的 MongoFixture.cs 程式內容

因為不會執行非同步方法,所以原本所繼承的 IAsyncLifetime 介面和兩個實做方法就需要移除,將建立 docker-image 與 docker-container 的部分就會在建構式裡執行,而當所有測試案例都執行完畢要結束測試專案時需要結束 docker-container 的部分,MongoFixture 繼承實做 IDosposable 介面,將移除 docker-container 的部分在 Dispose 方法裡執行。

以下就是修改後的 MongoFixture.cs 程式內容

using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.Services;
using Sample.Repositories.Infrastructure;
using FluentAssertions;

namespace Sample.RepositoriesTests;

public class MongoFixture : IDisposable
{
    private ICompositeService _compositeService;

    public MongoFixture()
    {
        this.CreateMongoDockerContainer();

        // FluentAssertions - Setup DateTime AssertionOptions
        SetupDateTimeAssertions();

        // MongoDb Class Mapping
        Mapping.RegisterClassMapping();
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (disposing is false)
        {
            return;
        }

        this._compositeService.Dispose();
        this._compositeService = null;
    }

    /// <summary>
    /// 建立測試用的 MongoDB Docker-image 與 Docker-Container.
    /// </summary>
    private void CreateMongoDockerContainer()
    {
        ProjectFixture.DatabaseIp = "127.0.0.1";
        ProjectFixture.DatabasePort = GetRandomPort();

        var dockerComposeFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Docker", "Mongo", "docker-compose.yml");

        var fileExists = File.Exists(dockerComposeFilePath);
        if (fileExists is false)
        {
            throw new FileNotFoundException(message: "file not found.", fileName: dockerComposeFilePath);
        }

        var dockerComposeContent = File.ReadAllText(dockerComposeFilePath);

        const string portString = "- 27017:27017";
        var newPortString = $"- {ProjectFixture.DatabasePort}:27017";

        dockerComposeContent = dockerComposeContent.Replace(portString, newPortString);
        File.WriteAllText(dockerComposeFilePath, dockerComposeContent);

        // 使用 FluentDocker (https://github.com/mariotoffia/FluentDocker)
        // 透過 FluentDocker 的 UseCompose 功能執行 docker-compose.yml
        // 執行 docker-compose 就會建立 MongoDB docker-image 與 docker-container

        this._compositeService = new Builder().UseContainer().UseCompose().FromFile(dockerComposeFilePath).RemoveOrphans().Build().Start();
    }

    /// <summary>
    /// Get Random Port
    /// </summary>
    /// <returns></returns>
    private static ushort GetRandomPort()
    {
        var rnd = new Random();
        var result = rnd.Next(49152, 65535);
        return (ushort)result;
    }

    /// <summary>
    /// FluentAssertions - Setup DateTime AssertionOptions
    /// </summary>
    private static void SetupDateTimeAssertions()
    {
        // FluentAssertions 設定 : 日期時間使用接近比對的方式,而非完全一致的比對
        AssertionOptions.AssertEquivalencyUsing(options =>
        {
            options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromMilliseconds(1000)))
                   .WhenTypeIs<DateTime>();

            options.Using<DateTimeOffset>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromMilliseconds(1000)))
                   .WhenTypeIs<DateTimeOffset>();

            return options;
        });
    }
}

應該不需要對上面的程式內容多加說明吧!

完全只有用到 FluentDocker 而已,沒有使用到 Testcontainers。


FluentDocker 所提供的功能不會比 Testcontainers 來得少,而且多了很多設定的功能,適合想要在執行時需要動手去做各種設定的開發人員,例如 Networking、Volume、Logging、Docker Compose 等等

Testcontainers 與 FluentDocker 都是好用的工具,沒有說一定要用哪一種,就依據自己的習慣、喜好來決定,或者混著用也可以。

Testcontainers

FluentDocker

Youtube

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力