测试的目的保证软件的质量和功能。测试的分类有很多种,下面主要是按开发阶段的测试方法;
1.单元测试:对软件中最小可测试单元进行检查和验证。一般情况下最小可测试单元如一个类,一个函数,一个方法等。
2.集成测试:对整个模块功能的正确性、单元模块之间接口的正确性、单个模块的缺陷对整个模块功能的影响、模块之间功能的冲突、全局数据结构的测试,
3.系统测试:对系统的功能、界面、兼容性、安全性、性能、可靠性、易用性、容错性;
4.E2E测试:将应用程序与其依赖的系统一起进行测试,确保在使用网络后,前后端程序(包括上下游系统)能顺畅交互,从而保证业务上实现闭环,确保满足客户的使用需求,可以帮助发现与系统相关的问题。
AAA模式即Arrange,Act,Assert:
Arrange(准备):在这个部分,我们准备测试所需的对象实例、数据和环境。这包括创建类的实例、设置输入参数和初始化测试数据。
Act(操作):在这个部分,我们执行要测试的操作或调用要测试的方法。这是对被测试代码进行实际操作的步骤。
Assert(断言):在这个部分,我们验证操作的结果是否符合预期。我们使用断言方法来检查实际输出与期望输出之间的匹配性。
一个单元测试的例子
// 定义一个接口
public interface ICalculator
{
int Add(int a, int b);
}
// 要测试的类
public class MyCalculator
{
private ICalculator _calculator;
public MyCalculator(ICalculator calculator)
{
_calculator = calculator;
}
public int AddTwoNumbers(int x, int y)
{
// 调用ICalculator接口的方法
return _calculator.Add(x, y);
}
}
// 单元测试
[Fact]
public void TestAddTwoNumbers()
{
// 创建一个模拟对象代替ICalculator接口的实现
var calculator = Substitute.For<ICalculator>();
// 设置模拟对象的方法的行为
calculator.Add(1, 2).Returns(3);
// 创建要测试的类的实例,并传入模拟对象
var myCalculator = new MyCalculator(calculator);
// 调用要测试的方法
int result = myCalculator.AddTwoNumbers(1, 2);
// 验证调用方法的行为和结果
result.ShouldBe(3);
// 验证被调用的方法是否被调用了指定的次数
calculator.Received().Add(1, 2);
}
//partial关键字:可在命名空间中定义该类、结构或接口的其他部分。 所有部分都必须使用 partial 关键字。在编译时,各个部分都必须可用来形成最终的类型。
public partial class TestBase : TestUtilbase, IAsyncLifetime, IDisposable
{
private readonly string _testTopic;//测试主题
private readonly string _databaseName;//数据库名
private static readonly ConcurrentDictionary<string, IContainer> Containers = new();//key:测试主题,value:配置容器
private static readonly ConcurrentDictionary<string, bool> shouldRunDbUpDatabases = new();//是否需要运行Dbup类
protected ILifetimeScope CurrentScope { get; }//生命周期范围
protected IConfiguration CurrentConfiguration => CurrentScope.Resolve<IConfiguration>();//get:当前生命周期内解析出配置容器
protected TestBase(string testTopic, string databaseName)
{
_testTopic = testTopic;
_databaseName = databaseName;
var root = Containers.GetValueOrDefault(testTopic);//获取测试容器
if (root == null)//当前容器不存在就新建一个容器
{
var containerBuilder = new ContainerBuilder();
var configuration = Registerconfiguration(containerBuilder);
RegisterBaseContainer(containerBuilder, configuration);
root = containerBuilder.Build();
Containers[testTopic] = root;
}
CurrentScope = root.BeginLifetimeScope();//从根容器创建一个新的子范围。在Autofac中,范围用于管理组件和服务实例的生命周期。当在范围内解析组件时,Autofac会跟踪在该范围内创建的实例,并在范围被销毁时对它们进行处理。
RunDbUpIfRequired();//如果需要运行DbUp数据库则运行它
SetupScope(CurrentScope);//设置生命周期
}
}
public partial class TestBase
{
//用来存储需要排除的表名
private readonly List<string> _tableRecordsDeletionExcludeList = new()
{
"schemaversions"
};
//运行DbUp数据库迁移
private void RunDbUpIfRequired()
{
//检查字典中是否存在指定数据库名称
if (!shouldRunDbUpDatabases.GetValueOrDefault(_databaseName, true)) return;
//存在,执行数据库迁移
new DbUpRunner(new ConnectionString(CurrentConfiguration).Value).Run();
//将字典中的数据库名设置为false,表示已经迁移过
shouldRunDbUpDatabases[_databaseName] = false;
}
//在容器注册Module和IMemoryCache。
private void RegisterBaseContainer(ContainerBuilder containerBuilder, IConfiguration configuration)
{
containerBuilder.RegisterModule(
new PractiseForJohnnyModule(configuration, typeof(PractiseForJohnnyModule).Assembly));
containerBuilder.RegisterInstance(Substitute.For<IMemoryCache>()).AsImplementedInterfaces();
}
//注册配置
private IConfigurationRoot Registerconfiguration(ContainerBuilder containerBuilder)
{
var targetJson = $"appsettings{_testTopic}.json";
File.Copy("appsettings.json", targetJson, true);//将appsettings.json复制到targetJson
dynamic jsonObj = JsonConvert.DeserializeObject(File.ReadAllText(targetJson));//JsonConvert.DeserializeObject方法将targetJson文件的内容解析为动态对象jsonObj,
jsonObj["ConnectionStrings"]["Default"] =
jsonObj["ConnectionStrings"]["Default"].ToString()
.Replace("Database=smart_faq", $"Database={_databaseName}");//修改其中的连接字符串,将数据库名称替换为_databaseName。
File.WriteAllText(targetJson, JsonConvert.SerializeObject(jsonObj));//然后将修改后的jsonObj重新写入到targetJson文件中
var configuration = new ConfigurationBuilder().AddJsonFile(targetJson).Build();//使用ConfigurationBuilder将其添加为配置
containerBuilder.RegisterInstance(configuration).AsImplementedInterfaces();//将configuration实例注册为containerBuilder的实现接口
return configuration;//返回configuration对象
}
public async Task InitializeAsync()
{
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
//清除数据库中的记录
private void ClearDatabaseRecord()
{
try
{
var connection = new MySqlConnection(new ConnectionString(CurrentConfiguration).Value);
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();//将_databaseName中的表名全部读取出来
deleteStatements.Add($"SET SQL_SAFE_UPDATES = 0");//创建一个空的deleteStatements列表,用于存储删除记录的SQL语句。并且添加了"SET SQL_SAFE_UPDATES = 0"语句,这是为了禁用安全更新模式。
//通过循环读取DataReader对象中的数据,获取每个表的名称,并检查是否在排除列表中。如果不在排除列表中,则向deleteStatements列表中添加"DELETE FROM {table}"语句,用于删除该表中的所有记录。
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) + ";";//将deleteStatements列表中的SQL语句通过string.Join方法连接成一个完整的SQL语句,并在末尾添加分号。
new MySqlCommand(strDeleteStatements, connection).ExecuteNonQuery();
connection.Close();
}
catch (Exception ex)
{
Console.WriteLine($"Error cleaning up data, {_testTopic}, {ex}");
}
}
public class TestUtilbase
{
private ILifetimeScope _scope;
protected TestUtilbase(ILifetimeScope scope = null)
{
_scope = scope;
}
protected void SetupScope(ILifetimeScope scope) => _scope = scope; //子类通过这个方法将scope赋值给_scope
protected void Run<T>(Action<T> action, Action<ContainerBuilder> extraRegistration = null)//无放回值
{
//容器存在注册额外组件,再解析出T类型,不存在直接解析T类型
var dependency = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration).Resolve<T>()
: _scope.BeginLifetimeScope().Resolve<T>();
action(dependency);
}
protected void Run<T, R>(Action<T, R> action, Action<ContainerBuilder> extraRegistration = null)//两个参数无返回值
{
var lifetime = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = lifetime.Resolve<T>();
var dependency2 = lifetime.Resolve<R>();
action(dependency, dependency2);
}
protected void Run<T, R, L>(Action<T, R, L> action, Action<ContainerBuilder> extraRegistration = null)//三个无返回值
{
var lifetime = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = lifetime.Resolve<T>();
var dependency2 = lifetime.Resolve<R>();
var dependency3 = lifetime.Resolve<L>();
action(dependency, dependency2, dependency3);
}
protected async Task Run<T, R, L>(Func<T, R, L, Task> action, Action<ContainerBuilder> extraRegistration = null)//异步无返回
{
var lifetime = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = lifetime.Resolve<T>();
var dependency2 = lifetime.Resolve<R>();
var dependency3 = lifetime.Resolve<L>();
await action(dependency, dependency2, dependency3);
}
protected async Task Run<T>(Func<T, Task> action, Action<ContainerBuilder> extraRegistration = null)//异步一参数无返回值
{
var dependency = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration).Resolve<T>()
: _scope.BeginLifetimeScope().Resolve<T>();
await action(dependency);
}
protected async Task RunWithUnitOfWork<T>(Func<T, Task> action, Action<ContainerBuilder> extraRegistration = null)//异步一参数无返回值保存
{
var scope = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = scope.Resolve<T>();
var unitOfWork = scope.Resolve<IUnitOfWork>();
await action(dependency);
await unitOfWork.SaveChangesAsync();
}
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;
}
protected async Task<R> RunWithUnitOfWork<T, U, R>(Func<T, U, Task<R>> action, Action<ContainerBuilder> extraRegistration = null)//异步两参数有放回值保存
{
var scope = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = scope.Resolve<T>();
var dependency1 = scope.Resolve<U>();
var unitOfWork = scope.Resolve<IUnitOfWork>();
var result = await action(dependency, dependency1);
await unitOfWork.SaveChangesAsync();
return result;
}
protected async Task RunWithUnitOfWork<T, U>(Func<T, U, Task> action, Action<ContainerBuilder> extraRegistration = null)//异步两参数无返回值保存
{
var scope = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = scope.Resolve<T>();
var dependency2 = scope.Resolve<U>();
var unitOfWork = scope.Resolve<IUnitOfWork>();
await action(dependency, dependency2);
await unitOfWork.SaveChangesAsync();
}
protected R Run<T, R>(Func<T, R> action, Action<ContainerBuilder> extraRegistration = null)//一参数一返回值
{
var dependency = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration).Resolve<T>()
: _scope.BeginLifetimeScope().Resolve<T>();
return action(dependency);
}
protected R Run<T, U, R>(Func<T, U, R> action, Action<ContainerBuilder> extraRegistration = null)//两参数一返回值
{
var lifetime = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = lifetime.Resolve<T>();
var dependency2 = lifetime.Resolve<U>();
return action(dependency, dependency2);
}
protected Task Run<T, U>(Func<T, U, Task> action, Action<ContainerBuilder> extraRegistration = null)//异步两参数无返回值
{
var lifetime = extraRegistration != null
? _scope.BeginLifetimeScope(extraRegistration)
: _scope.BeginLifetimeScope();
var dependency = lifetime.Resolve<T>();
var dependency2 = lifetime.Resolve<U>();
return action(dependency, dependency2);
}
}
public class TestUtil : TestUtilbase
{
protected TestUtil(ILifetimeScope scope)
{
SetupScope(scope);
}
//从程序集获取resourceName的流,并读取返回字符串
protected string ReadJsonFileFromResource(string resourceName)
{
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (stream == null) return string.Empty;
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
[Collection("Food Tests")]
public class FoodFixtureBase : TestBase
{
protected FoodFixtureBase() : base("_food_", "Test")//_food_:测试标题,Test:数据库名;
{
}
}
public partial class FoodFixture : FoodFixtureBase
{
private readonly FoodsUtil _foodsUtil;
public FoodFixture()
{
_foodsUtil = new FoodsUtil(CurrentScope);
}
[Fact]
public async Task CanCreateFood()
{
var food = new CreateFoodDto { Name = "mike", Color = "white" };
await Run<IRepository>(async repository =>
{
var beforeCreateFood = await repository.CountAsync<Foods>(x => true).ConfigureAwait(false);
beforeCreateFood.ShouldBe(0);
await _foodsUtil.CreateFoodAsync(food);
var afterUpdateFood = await repository.FirstOrDefaultAsync<Foods>(i => i.Name.Equals("mike")).ConfigureAwait(false);
afterUpdateFood?.Color.ShouldBe("white");
afterUpdateFood?.Name.ShouldBe("mike");
});
}
[Fact]
public async Task CanUpdateFood()
{
await RunWithUnitOfWork<IRepository>(async repository =>
await repository.InsertAsync<Foods>(new Foods { Id = 11, Name = "cake", Color = "red" }).ConfigureAwait(false));
var food = new UpdateFoodDto { Id = 11, Name = "mike", Color = "white" };
var beforeUpdateFood = await Run<IRepository, Foods>(async repository =>
await repository.GetByIdAsync<Foods>(11).ConfigureAwait(false));
beforeUpdateFood.Id.ShouldBe(11);
beforeUpdateFood.Name.ShouldBe("cake");
beforeUpdateFood.Color.ShouldBe("red");
await _foodsUtil.UpdateFoodAsync(food);
var afterUpdateFood = await Run<IRepository, Foods>(async repository =>
await repository.GetByIdAsync<Foods>(food.Id).ConfigureAwait(false));
afterUpdateFood.Id.ShouldBe(11);
afterUpdateFood.Name.ShouldBe("mike");
afterUpdateFood.Color.ShouldBe("white");
}
[Fact]
public async Task CanDeleteFood()
{
await RunWithUnitOfWork<IRepository>(async repository =>
await repository.InsertAsync<Foods>(new Foods { Id = 11, Name = "cake", Color = "red" }).ConfigureAwait(false));
var food = new DeleteFoodDto() { Id = 11 };
var beforeDeleteFood = await Run<IRepository, Foods>(async respository =>
await respository.GetByIdAsync<Foods>(food.Id));
beforeDeleteFood.Id.ShouldBe(11);
await _foodsUtil.DeleteFoodAsync(food);
var afterDeleteFood = await Run<IRepository, Foods>(async respository =>
await respository.GetByIdAsync<Foods>(food.Id));
afterDeleteFood.ShouldBeNull();
}
[Fact]
public async Task CanGetFood()
{
await RunWithUnitOfWork<IRepository>(async repository =>
await repository.InsertAsync<Foods>(new Foods { Id = 11, Name = "cake", Color = "red" }).ConfigureAwait(false));
var food = new GetFoodDto { Id = 11 };
var getFood = await _foodsUtil.GetFoodAsync(food);
getFood.Result.Id.ShouldBe(11);
getFood.Result.Name.ShouldBe("cake");
getFood.Result.Color.ShouldBe("red");
}
}