简体   繁体   English

使用静态变量的单元测试C#/。NET类(单元测试过程隔离)

[英]Unit testing C#/.NET classes which make use of static variables (unit test process segregation)

I've got a codebase that makes use of static variables in a number of cases where that makes sense, for example flagging that something's already run once since launch, etc etc. 我有一个在一些有意义的情况下使用静态变量的代码库,例如,标记自启动以来已经运行过一次的代码等。

Of course, this can lead to issues with unit testing whereupon suddenly order matters and the outcome of a test on a method of such a class may depend on whether other code has been hit before, etc. My understand of TestTools.UnitTesting is that whenever I run a set of unit tests, any within the same project run within the same process, thus any static state is maintained from test to test, whereas a Unit Test project boundary also implies a process boundary and thus, if I run 3 tests from project A then a fourth from project B, state is maintained from 1>2>3 (in whatever order they run) but then 4 is virgin and any static state is default. 当然,这可能会导致单元测试出现问题,突然之间顺序变得很重要,并且对此类方法的测试结果可能取决于之前是否有其他代码被击中过,等等。我对TestTools.UnitTesting的理解是我运行一组单元测试,在同一项目中在同一过程中运行任何单元测试,因此在测试之间保持任何静态,而单元测试项目边界也隐含一个过程边界,因此,如果我从项目A,然后是项目B的第四个项目,状态从1> 2> 3(以它们运行的​​顺序)保持不变,但是4是纯状态,并且任何静态都是默认状态。

So now my questions are two: 所以现在我的问题是两个:

1) is my assessment correct that a unit test projects have a 1:1 relationship with processes when tests are run in a group (run all or run selected), or is there more nuance there that I'm missing? 1)我的评估是否正确,即当在一个组中运行测试(全部运行或选择运行)时,单元测试项目与流程之间具有1:1的关系,还是我缺少的细微差别?

2) Regardless, if I have a test that definitely needs fresh, default static state for the custom objects it uses and tests, do I have a more elegant option for creating it than giving it its own test project? 2)无论如何,如果我有一个测试绝对需要其使用和测试的自定义对象的默认默认静态状态,那么与为其提供自己的测试项目相比,我是否有一个更优雅的选择来创建它?

Your assessment is correct as far as I know -- the assembly is loaded at the start of the test process, and any static state is maintained throughout the tests. 据我所知,您的评估是正确的-在测试过程开始时就加载了程序集,并且在整个测试过程中都保持了静态。

You should always start with a "fresh" state. 您应该始终以“新鲜”状态开始。 Unit tests should be able to be run in any order, with no dependencies whatsoever. 单元测试应该能够以任何顺序运行,而没有任何依赖性。 The reason is because your tests need to be reliable -- a test should only ever fail for one reason: The code it's testing changed. 原因是因为您的测试需要可靠-测试只能因以下原因而失败:测试的代码已更改。 If you have tests that depend on other tests, then you can easily end up with one test failing and "breaking the chain" such that a dozen other tests fail. 如果您有依赖于其他测试的测试,则很容易导致一个测试失败并“破坏链条”,从而使其他十几个测试失败。

You can use the TestInitialize attribute to define a method that will run before every test that will reset the state to the baseline. 您可以使用TestInitialize属性来定义一个方法,该方法将在每次将状态重置为基准的测试之前运行。

Another way to enable this is to wrap your static state into a singleton, then put a "back door" into the singleton so that you can inject an instance of the singleton class, allowing you to configure the state of the application as part of arranging your test. 实现此目的的另一种方法是将静态状态包装为单例,然后在单例中放置“后门”,以便可以注入单例类的实例,从而允许您在安排程序时配置应用程序的状态您的测试。

Statics are not actually per process, but per application domain, represented by the AppDomain class. 静态实际上不是每个进程而是由AppDomain类表示的每个应用程序域。 A single process can have several AppDomains. 一个进程可以具有多个AppDomain。 AppDomains have their own statics, can provide sandboxing to partially trusted code, and can be unloaded allowing newer versions of the same assembly to be hot swapped without restarting the application. AppDomain具有自己的静态变量,可以为部分受信任的代码提供沙箱,并且可以卸载,从而允许在不重新启动应用程序的情况下对同一程序集的较新版本进行热交换。

Your test runner is likely creating a new AppDomain per test assembly so each assembly gets its own static variables. 测试人员可能会为每个测试程序集创建一个新的AppDomain,以便每个程序集都具有自己的静态变量。 You can create an AppDomain to do the same on the fly. 您可以创建一个AppDomain来即时执行相同操作。 This is not typically great for pure unit tests, but I've had to work with "rude" libraries that do all kinds of static initialization and caching that cannot be cleaned out or reset. 对于纯单元测试而言,这通常不是很好,但我不得不使用“粗鲁”的库来执行各种静态初始化和无法清除或重置的缓存。 In those sorts of integration scenarios it is very useful. 在这类集成方案中,它非常有用。

You can use this helper to run a simple delegate: 您可以使用此帮助程序来运行一个简单的委托:

public static class AppDomainHelper
{
    public static void Run(Action action)
    {
        var domain = AppDomain.CreateDomain("test domain");
        try
        {
            domain.DoCallBack(new CrossAppDomainDelegate(action));
        }
        finally
        {
            AppDomain.Unload(domain);
        }
    }
}

One caution is that the delegate action passed to Run cannot have any captured variables (as in from a lambda). 一种警告是,传递给Run的委托action不能具有任何捕获的变量(如来自lambda的变量)。 That doesn't work because the compiler will generate a hidden class that is not serializable and so it cannot pass through an AppDomain boundary. 那是行不通的,因为编译器将生成一个不可序列化的隐藏类,因此它无法通过AppDomain边界。

If you're not looking to test global state, but rather what you do with values you get from global state, you can work around it. 如果您不是要测试全局状态,而是要对从全局状态获得的值进行处理,则可以解决该问题。

Consider this simple class definition that uses some static property. 考虑使用一些静态属性的简单类定义。

public class Foo
{
   public int Bar(int baz)
   {
       return baz + GlobalState.StaticValue;
   }
}

You can refactor it like this. 您可以像这样重构它。

public class Foo
{
    public virtual int GetGlobalStaticValue
    {
        return GlobalState.StaticValue;
    }

    public virtual int Bar(int baz)
    {
        return baz + this.GetGlobalStaticValue();
    }
}

I added virtual s to the method definitions because that's particular to Rhino Mocks, but you get the idea - while running live, your class will pull global state as it does now, but it gives you the hooks to mock out the values that will be returned in your test scenario. 我在方法定义中添加了virtual ,因为这是Rhino Mocks特有的,但是您可以理解-在实时运行时,您的类将像现在一样拉动全局状态,但是它为您提供了模拟将要使用的值的钩子在您的测试场景中返回。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM