Asp.Net集成XUnit测试

一、 单元测试、集成测试与TDD、BDD的区别

1. 单元测试 vs 集成测试:

  • 单元测试是针对代码中的最小测试单位,通常是单个函数或方法进行测试,它的目标是检测代码中的错误和缺陷。单元测试通常由开发人员编写,它可以快速准确地识别代码中的问题,并有助于维护代码的可靠性和可维护性。

  • 集成测试则是对多个单元测试组成的模块或组件进行测试,以确保它们能够协同工作。集成测试通常由测试人员执行,它有助于检测不同模块之间的集成问题,并确保整个系统的稳定性。

2. TDD vs BDD:

  • TDD(测试驱动开发)是一种开发方法,它要求在编写代码之前先编写测试用例。这些测试用例被用来指导代码编写的过程,并且在编写代码后用来验证代码的正确性。TDD可以确保代码具有高度的可测试性,并且有助于减少代码中的缺陷。

  • BDD(行为驱动开发)是一种开发方法,它将测试用例视为行为规范。BDD的目标是确保开发人员和测试人员能够共同理解应用程序的行为,从而减少不必要的沟通障碍。BDD测试用例通常是从用户故事或需求规范中派生出来的,并且通常以自然语言编写。

二、 XUnit

官方文档(rider版)

  • 和别的框架比较不同的地方:不用[Test]而是使用[Fact],如他文档中解释道“事实是永远正确的测试。他们测试不变的条件。”而[Theory]理论是仅适用于一组特定数据的测试。

简单示例:

[Theory]
[InlineData(2, 2, 4)]
[InlineData(3, 3, 6)]
[InlineData(2, 2, 5)]
public void MyTheory(int x, int y, int sum)
{
   Assert.Equal(sum, Calculator.Add(x, y));
}

运行结果:


result

三、Shoudly

GitHub官方地址

  • 旧的断言方式:
Assert.That(contestant.Points, Is.EqualTo(1337));

报错信息:

Expected 1337 but was 0
  • Shoudly的断言方式:
contestant.Points.ShouldBe(1337);

报错信息:

contestant.Points should be 1337 but was 0

四、需要集成到项目中的步骤

1. 把整个测试流程都放在一个生命周期当中

IAsyncLifetime和IDisposable

它们的作用如下:

  • IAsyncLifetime:IAsyncLifetime接口包含两个异步方法,用于管理对象的生命周期。这两个方法分别是InitializeAsync和DisposeAsync。InitializeAsync方法在对象创建后立即调用,用于初始化对象的状态。DisposeAsync方法在对象销毁前调用,用于清理对象占用的资源。使用IAsyncLifetime接口可以确保对象在创建和销毁时都能够正确地初始化和清理。

  • IDisposable:IDisposable接口包含一个Dispose方法,用于释放对象占用的资源。Dispose方法在对象销毁前调用,用于清理对象占用的资源。使用IDisposable接口可以确保对象在销毁时能够正确地释放占用的资源,例如文件句柄、数据库连接、网络连接等。

因此要同时实现IAsyncLifetime, IDisposable以及InitializeAsync、DisposeAsync和Dispose方法

public class TestBase : IAsyncLifetime, IDisposable
{
    public async Task InitializeAsync()
    {
    }

    public void Dispose()
    {
    }

    public Task DisposeAsync()
    {
        return Task.CompletedTask;
    }
}

2. 根据原来的appsettings.json文件在根目录重新生成一份json文件,并修改其数据库的库名(测试时用不带数据仅有结构的新库)

    private IConfiguration RegisterConfiguration(ContainerBuilder containerBuilder)
    {
        var targetJson = $"appsettings{_testTopic}.json";

        File.Copy("appsettings.json", targetJson, true);

        dynamic jsonObj = JsonConvert.DeserializeObject(File.ReadAllText(targetJson));

        jsonObj["ConnectionStrings"]["你的连接字符串"] =
            jsonObj["ConnectionStrings"]["你的连接字符串"].ToString()
                .Replace("Database=正式的数据库名", $"Database={测试的数据库名}");

        File.WriteAllText(targetJson, JsonConvert.SerializeObject(jsonObj));

        var configuration = new ConfigurationBuilder().AddJsonFile(targetJson).Build();
        containerBuilder.RegisterInstance(configuration).AsImplementedInterfaces();

        return configuration;
    }

3. 重新把需要注册的配置搬到测试solution中

这里用的是Autofac的Module的注册方式

   private void RegisterBaseContainer(ContainerBuilder containerBuilder, IConfiguration configuration)
    {
        containerBuilder.RegisterModule(new 之前定义的注册Module());
    }

4. 开启生命周期后,判断是否需要根据sql文件跑DbUp

 xxx.BeginLifetimeScope();

 private static readonly ConcurrentDictionary<string, bool> ShouldRunDbUpDatabases = new();
 private void RunDbUpIfRequired()
 {
     if (!ShouldRunDbUpDatabases.GetValueOrDefault("测试的数据库名", true))
          return;

     new DbUpRunner("测试数据库的连接字符串").Run();
 
     ShouldRunDbUpDatabases[_databaseName] = false;
 }

5. 编写一个工具类,给不同情况(接口类型不同、返回类型不同)定制请求接口的方法

示例:

    protected async Task<R> Run<T, R>(Func<T, Task<R>> action, Action<ContainerBuilder> extraRegistration = null)
    {
        var dependency = extraRegistration != null
            ? _scope.BeginLifetimeScope(extraRegistration).Resolve<T>()
            : _scope.BeginLifetimeScope().Resolve<T>();
        return await action(dependency);
    }
    
    protected async Task<R> RunWithUnitOfWork<T, R>(Func<T, Task<R>> action, Action<ContainerBuilder> extraRegistration = null)
    {
        var scope = extraRegistration != null
            ? _scope.BeginLifetimeScope(extraRegistration)
            : _scope.BeginLifetimeScope();

        var dependency = scope.Resolve<T>();
        var unitOfWork = scope.Resolve<IUnitOfWork>();

        var result = await action(dependency);
        await unitOfWork.SaveChangesAsync();

        return result;
    }

一般来说,T是需要调用到的接口,R是结果类型。

6. 如果需要用到当前用户信息,可以提供一个默认的CurrentUser

public class TestCurrentUser : ICurrentUser
{
    public int Id { get; } = 1;

    public string UserName { get; set; } = "TEST_USER";
}

并在注册的地方加上

 containerBuilder.RegisterInstance(new TestCurrentUser()).As<ICurrentUser>();

7. 在生命周期结束的时候把数据库的测试数据删掉

private void ClearDatabaseRecord()
    {
        try
        {
            var connection = new MySqlConnection("测试数据库的字符串");

            var deleteStatements = new List<string>();

            connection.Open();

            using var reader = new MySqlCommand(
                    $"SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = '{_databaseName}';",
                    connection)
                .ExecuteReader();

            deleteStatements.Add($"SET SQL_SAFE_UPDATES = 0");
            while (reader.Read())
            {
                var table = reader.GetString(0);

                if (!_tableRecordsDeletionExcludeList.Contains(table))
                {
                    deleteStatements.Add($"DELETE FROM `{table}`");
                }
            }

            deleteStatements.Add($"SET SQL_SAFE_UPDATES = 1");

            reader.Close();

            var strDeleteStatements = string.Join(";", deleteStatements) + ";";

            new MySqlCommand(strDeleteStatements, connection).ExecuteNonQuery();

            connection.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error cleaning up data, {_testTopic}, {ex}");
        }
    }

上面代码的意思:

连接到数据库,查询该数据库中所有的表名,然后删除除了在排除列表中的表之外的所有表中的记录。
其中,SQL_SAFE_UPDATES被设置为0以允许不安全的更新操作,以便执行删除操作。最后,SQL_SAFE_UPDATES被设置为1以确保安全更新操作。

不太相关的拓展

在.NET框架中,STA和MTA是指针对COM组件进行多线程编程时使用的不同线程模型。

  • STA(Single-Threaded Apartment)是一种单线程模型,它要求在一个COM组件中所有的方法调用都在同一个线程中完成,因此COM组件只能在一个线程中被访问。在STA模型下,如果多个线程需要同时访问同一个COM组件,那么这些线程必须使用互斥机制来协调访问,从而保证线程安全。

  • MTA(Multithreaded Apartment)是一种多线程模型,它允许多个线程同时访问同一个COM组件,不需要使用互斥机制来协调访问。在MTA模型下,COM组件中的每个对象都可以在不同的线程中被访问,从而提高了并发性能。

在.NET框架中,通过使用ComVisible属性来设置STA或MTA模型。默认情况下,所有的.NET组件都是MTA模型,如果需要使用STA模型,则需要将ComVisible属性设置为true,并使用Thread类的SetApartmentState方法将线程设置为STA模式。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容