在Xunit e2e测试中正确播种InMemoryDatabase



我有一个。net 5解决方案,包含一个API项目和两个独立的基于XUnit的测试项目(一个是裸单元测试,另一个是集成/端到端测试)。

作为端到端测试的一部分,我在数据库中植入了一些测试数据。

直到昨天,所有测试都成功了。今天,我在我的套件中添加了更多的测试,测试开始表现不一致:

  • 在Visual Studio本地运行完整的套件成功了,所以我自信地将Azure DevOps推向流水线
  • 本地运行dotnet test成功
  • 单独运行新增的测试(3)当然成功
  • 在Azure DevOps上,一些旧的测试失败。有趣的是,连续两次运行产生了相同的测试失败。我不能运行第三次执行,因为我耗尽了所有的管道预算

注意今天Azure DevOps在欧洲地区遇到事件

错误是不同的。在一种情况下,调用应该返回6的数据库COUNT的REST方法返回0(!),而在另一种情况下,我有异常

System.ArgumentException : An item with the same key has already been added. Key: 1
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTable`1.Create(IUpdateEntry entry)
at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryStore.ExecuteTransaction(IList`1 entries, IDiagnosticsLogger`1 updateLogger)
at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryDatabase.SaveChanges(IList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(DbContext _, Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
at Web.TestUtilities.Seeder.Seed(SendGridManagerContext dbContext) in D:a1sTestUtilitiesSeeder.cs:line 27
at Web.WebApplicationFactory.InitDatabase() in D:a1sWebApplicationFactory.cs:line 164
at TestFixtureBase..ctor(WebApplicationFactory testFactory, ITestOutputHelper outputHelper) in D:a1sTestFixtureBase.cs:line 27
at Web.Tests.ControllerTests..ctor(WebApplicationFactory testFactory, ITestOutputHelper outputHelper) in D:a1sTestsControllerTests.cs:line 19

(我稍微修改了堆栈跟踪)

从两个错误之间的关系来看,我怀疑我输入数据库的方法是不正确的。

决一雌雄

我创建了WebApplicationFactory类

public class MyWebApplicationFactory : WebApplicationFactory<Startup>
{
public ITestOutputHelper Output { protected get; set; }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.UseUrls("https://localhost:5001")
.ConfigureLogging(logging => logging
.ClearProviders()
.AddXUnit(Output)
.AddSimpleConsole())
.ConfigureTestServices(services =>
{
services.AddLogging(log =>
log.AddXUnit(Output ?? throw new Exception(
$"{nameof(Output)} stream must be set prior to invoking configuration. It should be done in the test base fixture")));
services.Remove(services.SingleOrDefault(service =>
service.ServiceType == typeof(DbContextOptions<MyDbContext>)));
services.AddDbContext<MyDbContext>(options =>
options.UseInMemoryDatabase("sg_inmemory"));
services.Configure<JwtBearerOptions>(.......

services.AddSingleton(.......
})
;
}

protected override IHostBuilder CreateHostBuilder()
{
return base.CreateHostBuilder()
.ConfigureLogging(log =>
log.AddXUnit()
);
}
public HttpClient CreateHttpClientAuthenticatedUsingMicrosoftOidc()
{
}
public async Task<HttpClient> CreateHttpClientAuthenticatedUsingPrivateOidcAsync(
CancellationToken cancellationToken = default)
{
}
public void InitDatabase()
{
using var scope = Services.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
dbContext.Database.EnsureCreated();
dbContext.Seed(); //Extension method defined elsewhere
}
public void DestroyDatabase()
{
using var scope = Services.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
dbContext.Database.EnsureDeleted();
}
}

To reallyseed数据库我创建了自己的扩展方法

public static void Seed(this MyDbContext dbContext)
{
using var tx = new TransactionScope();
dbContext.MyEntity.AddRange(GetSeedData());
dbContext.SaveChanges();
tx.Complete();
}

为了在测试中调用DB init/destroy循环,我利用构造函数和处理器

public class ControllerTests : TestFixtureBase
{
public ControllerTests(MyWebApplicationFactory testFactory, ITestOutputHelper outputHelper)
: base(testFactory, outputHelper)
{
}
// Let's talk about test code later
}

这个类继承自相同的fixture

public abstract class TestFixtureBase : IClassFixture<MyWebApplicationFactory>, IDisposable
{
private CancellationTokenSource _cancellationTokenSource => Debugger.IsAttached
? new CancellationTokenSource()
: new CancellationTokenSource(TimeSpan.FromMinutes(2));
protected MtWebApplicationFactory TestFactory { get; }
protected HttpClient PublicHttpClient => TestFactory.CreateClient();
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
protected TestFixtureBase(MyWebApplicationFactory testFactory,
ITestOutputHelper outputHelper)
{
TestFactory = testFactory;
TestFactory.Output = outputHelper;
TestFactory.InitDatabase();
}

~TestFixtureBase() => Dispose(false);

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
TestFactory.DestroyDatabase();
PublicHttpClient.Dispose();
_cancellationTokenSource.Dispose();
}
}
}

新添加的测试与现有测试相似。请注意失败的测试是在push之前通过的旧测试,并且我只是添加了功能

[Fact]
public async Task TestStatistics_ReturnsNull() // tests that REST controller returns empty JSON array []
{
var client = TestFactory.CreateHttpClientAuthenticatedUsingMicrosoftOidc();
var message = await client.GetAsync(
"/api/v1/secure/Controller/statistics/?Environment.IsNull=true",
CancellationToken); //shall return empty
message.EnsureSuccessStatusCode();
var result =
await message.Content.ReadFromJsonAsync<List<StatisticsDTO>>(
cancellationToken: CancellationToken);
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task TestCount_NoFilter()
{
var client = TestFactory.CreateHttpClientAuthenticatedUsingMicrosoftOidc();
var message = await client.GetAsync("/api/v1/secure/Controller/count",
CancellationToken);
message.EnsureSuccessStatusCode();
var result = await message.Content.ReadFromJsonAsync<int>();
int expected = Seeder.GetSeedData().Count(); //The seeder generates 6 mock records today
Assert.Equal(expected, result); //Worked till last push
}

我的调查和问题

我怀疑由于异步性,可能在某个点上测试正好在另一个处理测试清除数据库后运行,并且对于重复键错误,我很难理解它。

这是我有根据的猜测,我在播种数据库时做错了什么。

目前我的要求是REST应用程序启动时在内存数据库中准备了一些模拟记录。虽然数据库在内存中并且是短暂的,但我尝试做一些有经验的练习来清理数据库,因为这些代码将作为示例的一部分与其他开发人员学生分享,以教他们正确的模式。请允许我坚持清除内存数据库。

最后,在管道代码中没有什么特别的(我在开始时做了dotnet restore)

- task: DotNetCoreCLI@2
displayName: "Execute tests"
inputs:
command: 'test'
projects: |
*Tests/*.csproj
arguments: '--no-restore'

实际上,解决方案是在每个测试中使用一个总是不同的数据库标识符。

根据@Fabio的评论,每次调用lambda表达式services.AddDbContext<SendGridManagerContext>(options => options.UseInMemoryDatabase(???));时,我都必须生成一个数据库名称,但这恰好经常被调用,因为对象具有原型范围(yes),本文是关于Spring for Java的,但同样的原则也适用)。

实际上,在这种情况下,每次实例化DbContext时都会重新生成guid。

解决方案吗?生成一个随机id,但在整个测试过程中使其固定。

正确的位置是在测试工厂类

public class MyWebApplicationFactory : WebApplicationFactory<Startup>
{
public ITestOutputHelper Output { protected get; set; }
private readonly string dataContextName = $"myCtx_{Guid.NewGuid()}"; //Even plain Guid is ok, as soon as it's unique
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.UseUrls("https://localhost:5001")
.ConfigureLogging(...)
.ConfigureTestServices(services =>
{   
services.AddDbContext<MyDbContext>(options =>
options.UseInMemoryDatabase(dataContextName));
})
;
}
}

相关内容

  • 没有找到相关文章

最新更新