当前位置: 首页 > 知识库问答 >
问题:

与其他测试一起运行时,异步本地测试失败

何浩荡
2023-03-14

我正在尝试创建一个名为<code>AuditScope<code>的自定义范围类,通过<code>AuditScope.current<code>可以访问当前范围。

如果有嵌套的作用域,则当前作用域是嵌套最多的作用域。

我希望这是线程安全的,所以我使用AsyncLocal来确保当前范围属于当前异步上下文,并且不会与其他请求发生冲突。如果你们有人遇到过,这类似于TransactionScope类。

下面是我的作用域类:

public sealed class AuditScope : IDisposable
{
    private static readonly AsyncLocal<Stack<AuditScope>> ScopeStack = new();

    public int ExecutedByUserId { get; }

    public AuditScope(int executedByUserId)
    {
        ExecutedByUserId = executedByUserId;

        if (ScopeStack.Value == null)
        {
            ScopeStack.Value = new Stack<AuditScope>();
        }

        ScopeStack.Value.Push(this);
    }

    public static AuditScope? Current
    {
        get
        {
            if (ScopeStack.Value == null || ScopeStack.Value.Count == 0)
            {
                return null;
            }

            return ScopeStack.Value.Peek();
        }
    }

    public void Dispose()
    {
        ScopeStack.Value?.Pop();
    }
}

我的所有测试都单独通过,但是如果我同时运行所有测试,有一个测试总是失败:

[Test]
public async Task GivenThreadCreatesScope_AndSecondThreadCreatesScope_WhenCurrentScopeAccessedOnBothThreads_ThenCorrectScopeReturned()
{
    // Arrange
    static async Task createScopeWithLifespan(int lifespanInMilliseconds)
    {
        // This line throws the error, saying it is not null (for the 2000ms scope)
        // No scope has been created yet for this async context, so current should be null
        Assert.IsNull(AuditScope.Current);

        using (var scope = new AuditScope(1))
        {
            // Scope has been created, so current should match
            Assert.AreEqual(scope, AuditScope.Current);

            await Task.Delay(lifespanInMilliseconds);

            // Scope has not been disposed, so current should still match
            Assert.AreEqual(scope, AuditScope.Current);
        }

        // Scope has been disposed, so current should be null
        Assert.IsNull(AuditScope.Current);
    }

    // Act & Assert
    await Task.WhenAll(
        createScopeWithLifespan(1000),
        createScopeWithLifespan(2000));
}

当然,由于使用语句的在不同的上下文中,这应该可以工作吗?为什么在单独运行时会通过,而在与其他测试一起运行时不会通过?

为了完整起见,下面是我正在运行的其他测试,但我严重怀疑它们是否会直接影响到它们:

[Test]
public void GivenNoCurrentScope_WhenCurrentScopeAccessed_ThenNull()
{
    // Act
    var result = AuditScope.Current;

    // Arrange
    Assert.Null(result);
}

[Test]
public void GivenScope_WhenScopeDisposed_ThenNull()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
    }

    // Act
    var result = AuditScope.Current;

    // Arrange
    Assert.Null(result);
}

[Test]
public void GivenScopeCreated_WhenCurrentScopeAccessed_ThenScopeReturned()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        // Act
        var result = AuditScope.Current;

        // Arrange
        Assert.NotNull(result);
        Assert.AreEqual(scope, result);
    }
}

[Test]
public void GivenNestedScopeCreated_WhenCurrentScopeAccessed_ThenNestedScopeReturned()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        using (var nestedScope = new AuditScope(2))
        {
            // Act
            var result = AuditScope.Current;

            // Arrange
            Assert.NotNull(result);
            Assert.AreEqual(nestedScope, result);
        }
    }
}

[Test]
public void GivenNestedScopeCreated_WhenNestedScopeDisposed_ThenCurrentScopeRevertsToParent()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        using (var nestedScope = new AuditScope(2))
        {
        }

        // Act
        var result = AuditScope.Current;

        // Arrange
        Assert.NotNull(result);
        Assert.AreEqual(scope, result);
    }
}

共有2个答案

拓拔奇
2023-03-14

我也有同样的问题。像我一样,我相信你的问题是在你的处置:

public void Dispose()
{
    ScopeStack.Value?.Pop();
}

如果你把它换成这个,我愿意打赌吗?

public void Dispose()
{
    ScopeStack.Value?.Pop();
    if(ScopeStack.Value.Count == 0)
         ScopeStack.Value = null;
}

...即使没有不变堆栈,您的代码也会按预期工作。

异步本地感觉类似于线程静态,并且在异步上下文(如 MVC)中使用时也是如此。它给出了您期望的每个调用上下文的相同分离,但解决了由不同线程提供服务的上下文所创建的问题。

但是与ThreadStatic不同,AsyncLocal上下文确实向下流到同一个调用上下文中的子线程。

在我们的单元测试中,初始调用上下文是测试运行器,它可能在同一线程中运行其他测试。当它到达有问题的测试时,ScopeStack被初始化,然后传递到子线程。您可能会在启动任务之前简单地创建并销毁ScopeStack,从而重现相同的错误。

将它设置为null允许它在线程内部重新初始化,而AsyncLocal不会流回(子线程都将初始化自己的线程,因为它们不会看到它已经建立)。

ImmutableStack修复了它,因为即使它向下流动,您也永远不会真正对原始版本进行更改。这里唯一的问题是,如果您想进行一些无效的嵌套检测或不允许在子范围之前从父范围进行处置,您将无法看到子范围所做的此类更改。当然,如果您需要从同一个父范围启动多个兄弟姐妹,这可能已经是遥不可及的了(此时他们不能共享一个堆栈)。

舒博雅
2023-03-14

事实证明,在某个地方一定存在引用问题,因为替换了Stack

 类似资料:
  • 我得堆栈: 想法2019.1.3 Springboot 2.1.6 Java 11 Maven 3.8.0 Groovy 2.5 史巴克1.3 JUnit jupiter 5.5.1 JUnit vintage 5.5.1 GMavenPlus插件2.7.1 我们想开始在Spock测试框架中编写测试。我跟着这个howto,但没有成功。当我尝试运行所有测试时,我的spock测试没有运行。 我能运行一

  • 我有一些cucumber场景运行顺利,但我也想运行其他类型的测试。例如,“测试页面上的每个组件”不是一个有效的方案,因为 BDD 是为了检查行为而创建的。我想划分cucumber场景和硒/成分测试 这是我的跑步者:

  • 我有一个异步函数,我想同时测试成功和失败。函数成功时返回一个字符串,失败时抛出。我在测试失败上失败得很惨。下面是我的代码: 我通过注释失败的代码并在注释中添加结果来禁用 正如你所看到的,什么都不起作用。我相信我的测试几乎是第一个失败测试的Promises示例和最后一个失败测试的Async/Await示例的完全副本,但是没有一个可以工作。 我相信与Jest文档中的示例的不同之处在于,它们展示了如何测

  • 完成了818个集成测试,0在104001ms运行1个spock测试时失败...失败:CreditServiceSpec groovy.lang.groovyRuntimeException:未能调用构造函数:public org.codehaus.groovy.grails.test.support.grailstestautoWirer(org.springframework.context.a

  • 不是只使用,我们使用来包装它并完成依赖并在异步过程中执行测试。 使用需要我们返回一个Promise,我们通过调用或者来解决我们的测试的competition ,这取决于我们测试的结果。

  • 我们已经为spark编写了单元测试,在本地模式下有4个线程。 当一个接一个地启动时,例如通过intellij或sbt testOnly,每个测试都运行良好。 当用sbt测试启动时,它们会出现如下错误 我们使用的是一个带有多个子项目的sbt项目,其定义如下: