You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

批量运行xUnit测试失败:InvalidOperationException排查求助

批量运行xUnit测试时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 20:59:54