批量运行xUnit测试失败:InvalidOperationException排查求助
AddRemoveEntry_ActiveMember_Success测试失败排查 错误信息
chldr_shared.tests.EntryServiceTests.AddRemoveEntry_ActiveMember_Success Source: EntryServiceTests.cs line 27 Duration: 1.9 sec Message: System.InvalidOperationException : Operation is not valid due to the current state of the object. Stack Trace: SqlEntriesRepository.Add(EntryDto newEntryDto) line 199 EntryService.AddEntry(EntryDto entryDto, String userId) line 24 EntryServiceTests.AddRemoveEntry_ActiveMember_Success() line 41 --- End of stack trace from previous location ---
批量运行所有xUnit测试时,上述测试抛出InvalidOperationException,但单独运行该测试完全正常。测试使用真实数据库作为数据源(符合设计预期),怀疑是测试之间隔离性不足导致数据混淆。
测试类代码
public class EntryServiceTests { private IDataProvider _dataProvider; private EntryService _entryService; private static string _userId; public EntryServiceTests() { _dataProvider = TestDataFactory.CreateSqlDataProvider(); _entryService = new EntryService(_dataProvider); var unitOfWork = _dataProvider.CreateUnitOfWork(); _userId = unitOfWork.Users.GetRandomsAsync(1).Result.First().UserId; } [Fact] public async Task AddRemoveEntry_ActiveMember_Success() { using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // Arrange var unitOfWork = _dataProvider.CreateUnitOfWork(_userId); var user = (await unitOfWork.Users.GetRandomsAsync(1)).First(); var source = (await unitOfWork.Sources.GetRandomsAsync(1)).First(); var entryDto = TestDataFactory.CreateRandomEntryDto(user.UserId, source.SourceId); // Act await _entryService.AddEntry(entryDto, user.UserId); // Assert var insertedEntry = await _entryService.Get(entryDto.EntryId); var userRateRange = user.GetRateRange(); Assert.Equal(entryDto.Content, insertedEntry.Content); Assert.Equal(userRateRange.Lower, insertedEntry.Rate); Assert.Equal(entryDto.Sounds[0].SoundId, insertedEntry.Sounds[0].SoundId); await _entryService.Remove(entryDto.EntryId, user.UserId); await Assert.ThrowsAsync<ArgumentException>(async () => await _entryService.Get(entryDto.EntryId)); } } [Fact] public async Task AddEntry_InActiveUser_Fails() { using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // Arrange var unitOfWork = _dataProvider.CreateUnitOfWork(_userId); var user = TestDataFactory.CreateRandomUserDto(); user.Status = chldr_data.Enums.UserStatus.Banned; await unitOfWork.Users.Add(user); var source = (await unitOfWork.Sources.GetRandomsAsync(1)).First(); var entryDto = TestDataFactory.CreateRandomEntryDto(user.UserId, source.SourceId); // Act await Assert.ThrowsAsync<InvalidOperationException>(async () => await _entryService.AddEntry(entryDto, user.UserId)); } } [Fact] public async Task Get_NonExistingEntity_ThrowsException() { using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // Arrange var entityId = "non_existing_entity_id"; // Act and Assert await Assert.ThrowsAsync<ArgumentException>(async () => await _entryService.Get(entityId)); } } }
SqlEntriesRepository.Add方法(第199行相关)
public override async Task<List<ChangeSetModel>> Add(EntryDto newEntryDto) { if (newEntryDto == null || string.IsNullOrEmpty(newEntryDto.EntryId)) { throw new NullReferenceException(); } var user = UserModel.FromEntity(await _dbContext.Users.FindAsync(_userId)); if (user.Status != UserStatus.Active) { // 第199行:理论上用户状态应为Active,但批量测试时触发此异常 throw new InvalidOperationException(); } // 设置评分 newEntryDto.Rate = user.GetRateRange().Lower; foreach (var translationDto in newEntryDto.Translations) { translationDto.Rate = newEntryDto.Rate; } try { // 插入Entry实体(关联声音和翻译实体) var entry = SqlEntry.FromDto(newEntryDto, _dbContext); _dbContext.Add(entry); _dbContext.SaveChanges(); // 更新本地entry的CreatedAt newEntryDto.CreatedAt = entry.CreatedAt; var entryChangeSet = CreateChangeSetEntity(Operation.Insert, newEntryDto.EntryId); var resultingChangeSets = new List<ChangeSetModel> { ChangeSetModel.FromEntity(entryChangeSet) }; // 处理新增的声音文件 foreach (var sound in newEntryDto.Sounds) { if (string.IsNullOrEmpty(sound.RecordingB64)) { continue; } var filePath = Path.Combine(_fileService.EntrySoundsDirectory, sound.FileName); File.WriteAllText(filePath, sound.RecordingB64); } return resultingChangeSets; } catch (Exception ex) { if (ex.InnerException != null && ex.Message.Equals("Could not save changes. Please configure your entity type accordingly.")) { throw _exceptionHandler.Error(ex.InnerException); } throw _exceptionHandler.Error(ex); } }
排查建议
测试数据库中仅存在两个Active状态的用户,失败测试理论上不应遇到非Active用户。推测是其他测试(如AddEntry_InActiveUser_Fails中创建的禁用用户)导致数据混淆,可从以下方向排查:
静态变量
_userId的共享问题:测试类中的_userId是静态字段,xUnit会复用测试类实例,可能导致不同测试使用同一个用户ID,若该用户被其他测试修改状态(或随机获取时命中新增的禁用用户)就会触发异常。将_userId改为非静态字段,确保每个测试实例使用独立的用户ID。事务隔离与范围问题:虽然每个测试都使用了
TransactionScope,需确认事务隔离级别是否足够(如设置为Serializable),避免其他测试的未提交事务影响当前测试的查询结果。同时检查TransactionScope的异步流配置是否正确,确保所有异步操作都在事务范围内执行。随机用户筛选问题:
GetRandomsAsync(1)可能在批量测试时返回其他测试新增的禁用用户。在AddRemoveEntry_ActiveMember_Success测试中明确筛选Active状态的用户,而非随机获取,确保拿到的用户状态合法。DbContext复用问题:检查
TestDataFactory.CreateSqlDataProvider()是否复用了DbContext实例,导致不同测试共享同一数据库连接,从而看到其他测试未提交的数据。确保每个测试使用独立的DbContext实例。测试并行执行问题:xUnit默认并行执行测试,可能导致
AddEntry_InActiveUser_Fails新增的禁用用户被AddRemoveEntry_ActiveMember_Success的随机查询获取到。可尝试禁用测试并行执行,或在每个测试执行前清理新增的测试数据。
内容的提问来源于stack exchange,提问作者Movsar Bekaev

