c# 单元测试_解决C#中的单元测试难题

c# 单元测试

To my convenience I have used certain tools and frameworks throughout the examples in this article and I assume the reader have knowledge on C# language, some experience writing Unit tests, any Mocking framework, and any NUnit test framework. 

为方便起见,我在本文的所有示例中都使用了某些工具和框架,并假定读者具有C#语言的知识,一些编写单元测试的经验,任何Mocking框架和任何NUnit测试框架。

What is being tested in a unit test?

单元测试中正在测试什么?

A unit test is always an independent method that invokes a target method (code under test) and ensures it doesn’t breaks and gives expected results in every possible situation. A simple unit test method (test scenario) would invoke the target method providing multiple inputs to it (test cases) and succeeds when expected result is delivered against each of those test cases (See Exhibit #1). Does that define a whole unit test? The answer is No. 

单元测试始终是一个独立的方法,该方法调用目标方法(被测代码),并确保它不会中断并在每种可能的情况下给出预期的结果。 一个简单的单元测试方法( 测试场景 )将调用目标方法,向它提供多个输入(测试用例),并在针对每个测试用例交付预期结果时成功(参见图表1)。 这是否定义了整个单元测试? 答案是不。

Exhibit #1

展览#1

Code under Test

被测代码

public decimal Multiply(decimal number1, decimal number2)
{
    return number1 * number2;
} 

Unit Test

单元测试

[Test]
public void Multiply_CanMultiplyDecimals([Values(0,234.5,2334890.234)]decimal number1, 
                                        [Values(2342.3214, -123123.2, 0)]decimal number2)
{
    //Arrange
    var program = new Calculator();

    //Act
    var product = program.Multiply(number1, number2);

    //Assert
    Assert.AreEqual(number1*number2, product);
} 

We developers understand that every piece of code we write has dependencies to other units of code and it is as such consumed by other units. And we are bothered that any future changes to those, should not break the piece of code we are writing. Hence output versus input is not the only aspect what matters, but shielding every line of code inside a method is also important. Thus by protecting my lines of code, I ensure any unintended changes made to the code fails my unit tests and warn the future developer to either rollback the changes or change unit tests to comply his changes. This is very important while writing unit tests and a basic unit test as we discussed in exhibit #1 might not help on this when we start writing tests for more complex functions than that of a basic multiplier.

我们的开发人员了解,我们编写的每段代码都具有对其他代码单元的依赖关系,并且其他代码单元也是如此。 而且我们很烦恼,将来对它们所做的任何更改都不应破坏我们正在编写的代码段。 因此,输出与输入并不是唯一重要的方面,但是屏蔽方法中的每一行代码也很重要。 因此,通过保护我的代码行, 我确保对代码进行的任何意料之外的更改都会使我的单元测试失败,并警告未来的开发人员要么回滚所做的更改,要么更改单元测试以符合其更改 。 这在编写单元测试时非常重要,而当我们开始编写比基本乘法器更复杂的函数的测试时,正如我们在展览1中所讨论的,基本单元测试可能对此无济于事。

How to ensure I am writing code as testable units?

如何确保我将代码编写为可测试单元?

Here are few thumb rules while writing a unit of code -

这是编写代码单元时的一些经验法则-

AccessibleFunction/Method you write is accessible from the test class

可访问的您编写的函数/方法可从测试类访问

A better way of keeping test classes is to include all of them into another project as such, suffixing name with Tests or UnitTests (e.g. MyProject.Tests.csproj). Hence, to access the target methods from this external library, method modifier should either be public or internal (with InternalsVisibleTo attribute applied to assembly)

保留测试类的一种更好的方法是将所有类都包含在另一个项目中,并在其后加上Tests或UnitTests的名称(例如MyProject.Tests.csproj)。 因此,要从此外部库访问目标方法,方法修饰符应该是公共的或内部的(将InternalsVisibleTo属性应用于程序集)

SimplifiedAvoid unnecessary private method calls, keep it simple and readable

简化避免不必要的私有方法调用,使其简单易读

As per Single Responsibility principle, each classes should be written to achieve a single functionality and each method within which should perform a single operation towards the goal. Try to avoid writing multiple methods in the same class to achieve a functionality and invoke one from the other unless you really need them to. If you got a reusable piece of code, identify its behavior, and consider refactor that to another class like a Utility class. This will help in keeping the code cleaner and readable too.

根据“单一职责”原则,应编写每个类以实现单个功能,并且在其中每个方法应针对目标执行单个操作。 除非您确实需要,否则请尝试避免在同一类中编写多个方法以实现功能并从另一个调用另一个方法。 如果您有一段可重用的代码,请确定其行为,然后考虑将其重构为另一个类,例如Utility类。 这也将有助于保持代码的简洁性和可读性。

IsolatedCode can be isolated from other implementations during test invocations

隔离在测试调用期间,可以将代码与其他实现隔离

To achieve the isolation for any piece of code, we should ensure the entities depends on abstractions and not concretions (as stated on Dependency inversion principle). All entities we consume within our piece of code should have an abstraction and declarations are made as read only private members. This helps to ensure them not being modified other than at constructor where we could inject our instance into. Use a default constructor to chain the other constructor to inject original concretions, thus application can continue using the default constructor and test methods could use overloaded constructor (see Exhibit #2). An alternative would be property injections using any of the IoC container (per the project standards).

为了实现任何代码段的隔离,我们应确保实体依赖于抽象而不依赖于具体(如依赖倒置原则所述)。 我们在代码段中使用的所有实体都应具有抽象,并且声明应作为只读私有成员进行。 这有助于确保除了在我们可以将实例注入其中的构造函数之外,不要修改它们。 使用默认构造函数链接其他构造函数以注入原始混凝土,因此应用程序可以继续使用默认构造函数,而测试方法可以使用重载构造函数( 请参见图表2 )。 一种替代方法是使用任何IoC容器(根据项目标准)进行属性注入。

Exhibit #2

展品#2

public class Repository
{
    private readonly IConnectionManager _connectionManager;
    private readonly IConfigurationManagerFacade _configurationManagerFacade;
    private readonly ILogger _logger;
    // Unit test uses this ctor()
    public Repository(IConnectionManager connectionManager, 
                    IConfigurationManagerFacade configurationManagerFacade, ILogger logger)
    {
        _connectionManager = connectionManager;
        _configurationManagerFacade = configurationManagerFacade;
        _logger = logger;
    }


    // application uses this ctor()
    public Repository() : this(new ConnectionManager(), new ConfigurationManagerFacade(),
                                new Logger())
    {
    }
} 

When it comes to isolation of the code, there are few challenges developers might face. For instance, when we use static method calls as on Exhibit #3 where developer cannot rely on an abstraction and obviously no concretion as well.

当涉及到代码隔离时,开发人员可能会面临很少的挑战。 例如,当我们在图表3中使用静态方法调用时,开发人员无法依赖抽象,显然也不能依赖任何具体方法。

Exhibit #3

展览#3

var connectionString = ConfigurationManager.ConnectionStrings[connectionName]

var connectionString = ConfigurationManager.ConnectionStrings [connectionName]

.ConnectionString;

.ConnectionString;

In these cases, best bet is to make use of a Facade pattern. See Exhibit #4 how problem discussed in Exhibit #3 has been solved by wrapping a static function call into a concretion that has an abstraction. This abstraction can be used in the target code to invoke the method and thus keep our thumb rule up.

在这些情况下,最好的选择是利用Facade模式 。 请参见图表4,如何通过将静态函数调用包装到具有抽象的构造物中来解决图表3中讨论的问题。 可以在目标代码中使用此抽象来调用该方法,从而保持我们的经验法则。

Exhibit #4

展览#4

public interface IConfigurationManagerFacade
{
    string GetConnectionString(string connectionName);
}


public class ConfigurationManagerFacade : IConfigurationManagerFacade
{
    public string GetConnectionString(string connectionName)
    {
        return ConfigurationManager.ConnectionStrings[connectionName].ConnectionString;
    }
} 

Usage in Exhibit #3 becomes -

图表3中的用法变为-

var connectionString = _configurationManagerFacade.GetConnectionString(ConnectionName);
 

See Exhibit #2 that depicts declaration and constructor injection of ConfigurationManagerFacade. This same approach shall be chosen for extension methods and to those types which developer do not own and does not have abstractions. We commonly face this issue while working with third party libraries and few .NET types like a StreamReader Class (System.IO). I have shown a pattern below that would make things a little easier (see Exhibit #5).

请参阅图2,该图描述了ConfigurationManagerFacade的声明和构造函数注入。 对于扩展方法和开发人员不拥有也不具有抽象的那些类型,应选择相同的方法。 在使用第三方库和少数.NET类型(例如StreamReader类(System.IO))时,我们通常会遇到此问题。 我在下面显示了一种模式,它会使事情变得容易一些(请参阅图5)。

Exhibit #5

展览#5

public abstract class NonStaticFacade<T> where T : class
{
    protected readonly T OriginalInstance;

    /// <summary>
    /// For facades that wraps a non static object, might require a null check on the 
    /// wrapped subject
    /// </summary>
    /// <returns>returns true if the wrapped object is null</returns>
    public bool IsNull()
    {
        return OriginalInstance == null;
    }

    /// <summary>
    /// Facade placeholder to return the original wrapped subject
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public T Get()
    {
        return OriginalInstance;
    }

    protected NonStaticFacade(T originalInstance)
    {
        this.OriginalInstance = originalInstance;
    }

    protected NonStaticFacade()
    {
    }
}
public class StreamReaderFacade : NonStaticFacade<StreamReader>, IStreamReaderFacade
{
    public StreamReaderFacade(StreamReader originalInstance) : base(originalInstance)
    {
    }

    public string ReadToEnd()
    {
        return OriginalInstance.ReadToEnd();
    }
} 

Thus, by wrapping the original instance inside a façade helps to access the entities through an abstraction. And to instantiate these facades, rather than injecting non static Facades through constructor we could use a static façade that do the job as shown in Exhibit #6

因此,通过将原始实例包装在外立面中,有助于通过抽象访问实体。 并且要实例化这些外观,而不是通过构造函数注入非静态外观,我们可以使用静态外观来完成工作,如图表6所示。

Exhibit #6

展览#6

public interface IStreamReaderStaticFacade
{
    IStreamReaderFacade Create(IStreamFacade receiveStream, Encoding encoding);
}


public class StreamReaderStaticFacade : IStreamReaderStaticFacade
{
    public IStreamReaderFacade Create(IStreamFacade receiveStream, Encoding encoding) 
    {
        return new StreamReaderFacade(new StreamReader(receiveStream.Get(), encoding));
    }
} 

Usage (see highlighted declarations)

用法(请参阅突出显示的声明)

public class SampleOnNonStaticFacade
{
    private readonly IStreamReaderStaticFacade _streamReaderStaticFacade;
    private readonly IWebRequestStaticFacade _webRequestStaticFacade;


    public SampleOnNonStaticFacade(IStreamReaderStaticFacade streamReaderStaticFacade, 
    IWebRequestStaticFacade webRequestStaticFacade)
    {
        _streamReaderStaticFacade = streamReaderStaticFacade;
        _webRequestStaticFacade = webRequestStaticFacade;
    }


    public string ReadFromStream(string url)
    {
        var webRequestFacade = _webRequestStaticFacade.Create(url);
        var authorization = "Basic " + 
            Convert.ToBase64String(Encoding.UTF8.GetBytes("username:password"));


        webRequestFacade.AddHeaders("Authorization", authorization);
        webRequestFacade.ContentType = "application/json";
        webRequestFacade.Method = "POST";


        var webResponse = webRequestFacade.GetResponse();
        using (var receiveStream = webResponse.GetResponseStream())
        {
            var streamReaderFacade = _streamReaderStaticFacade.Create(receiveStream, 
                                Encoding.UTF8);
            var responseString = streamReaderFacade.ReadToEnd();
            return responseString;
        }
    }
} 

The discussed pattern in Exhibit #6 becomes even challenging when we require original reference of the instance such as accessing a Context key value pair that keeps changing in the memory between our statements. I have faced this while working with OWIN collections in a Web API 2, the above pattern let us access the copy of an original instance and we need the reference of original instance as such. In these situations, a Lazy façade could help where instead of injecting original instance in the constructor we pass a Func<OriginalInstance> (See Exhibit #7).

当我们需要实例的原始引用(例如访问上下文键值对)时,图表#6中讨论的模式变得更具挑战性,例如在语句之间的内存中不断变化的上下文键值对。 我在使用Web API 2中的OWIN集合时遇到了这个问题,上述模式使我们可以访问原始实例的副本,因此我们需要引用原始实例。 在这种情况下,可以通过传递Func <OriginalInstance>而不是在构造函数中注入原始实例来实现惰性外观 (请参见图7)。

Exhibit #7

展览#7

public abstract class NonStaticFacadeLazy<T> where T : class
{
    protected readonly Func<T> OriginalInstanceFunc;


    public virtual bool IsNull()
    {
        return OriginalInstanceFunc.Invoke() == null;
    }

    public virtual T Get()
    {
        return OriginalInstanceFunc.Invoke();
    }

    protected NonStaticFacadeLazy(Func<T> originalInstanceFunc)
    {
        this.OriginalInstanceFunc = originalInstanceFunc;
    }

}
public class AppContextFacade : NonStaticFacadeLazy<AppContext>, IAppContextFacade 
{
    public AppContextFacade(Func<AppContext> originalInstance) : base(originalInstance)
    {
    }

    public ITransactionFacade CurrentTransaction => new 
            TransactionFacade(OriginalInstanceFunc.Invoke().CurrentTransaction); 
} 

How to write a Unit test?

如何编写单元测试?

The goal being the protection of original application code, we should ensure every line of written code is executed in the way I wrote the code, assignments are happening as expected and finally results are being passed on to right hands. For this example lets us choose Repository class we discussed in Exhibit #2 as the code under test. See the Retrieve() function in Exhibit #8 which is the method under test.

我们的目标是保护原始应用程序代码,我们应该确保编写的每一行代码都按照我编写代码的方式执行,分配工作按预期进行,最终结果将传递给正确的人。 对于此示例,让我们选择在图表2中讨论过的Repository类作为测试代码。 请参见图表8中的Retrieve()函数,它是被测试方法。

Exhibit #8

展览#8

public class Repository
{
    private readonly IConnectionManager _connectionManager;
    private readonly IConfigurationManagerFacade _configurationManagerFacade;
    private readonly ILogger _logger;

    private const string ConnectionName = "defaultConnection";

    // Unit test uses this ctor()
    public Repository(IConnectionManager connectionManager, 
                    IConfigurationManagerFacade configurationManagerFacade, ILogger logger)
    {
        _connectionManager = connectionManager;
        _configurationManagerFacade = configurationManagerFacade;
        _logger = logger;
    }

    // application uses this ctor()
    public Repository() : this(new ConnectionManager(), 
                            new ConfigurationManagerFacade(), new Logger())  
    {
    }

    public bool Retrieve(string storeProcedureName, Func<IDataReader, bool> readFunc)
    {
        var connectionString = 
                _configurationManagerFacade.GetConnectionString(ConnectionName);
        var connection = _connectionManager.CreateConnection(connectionString);
        var command = _connectionManager.CreateCommand(storeProcedureName);
        command.Connection = connection;

        connection.Open();
        bool success;
        using (var reader = command.ExecuteReader())
        {
            success = readFunc.Invoke(reader);
        }
        connection.Close();

        if (!success)
        {
            _logger.Trace("Failed to read data from {0}", storeProcedureName);
        }

        return success;
    }
} 

As the first step of writing the unit test, let us mock the members of target entity that helps to monitor the actions attempted on them within the code under test (see Exhibit #9). I am using NSubstitute as the Mock generator in the below example.

作为编写单元测试的第一步,让我们模拟目标实体的成员,这些成员有助于监视被测代码内对它们尝试的操作(参见图表9)。 在下面的示例中,我使用NSubstitute作为Mock生成器。

Exhibit #9

展览#9

[Test]
public void Retrieve_CanRetrieve()
{
    //Arrange
    var connectionManager = Substitute.For<IConnectionManager>();
    var configurationManagerFacade = Substitute.For<IConfigurationManagerFacade>();
    var logger = Substitute.For<ILogger>();
} 

Since we are injecting mock objects to the target entity, we can easily control the target code with more of our mock objects that are expected in each line of code. For instance, see the connection manager instance is supposed to create and return an IDbConnection instance on second line of target method, let us ask the connection manager mock to return our own mock connection there. Thus, let us add set of behaviors our primary mock objects should perform on each statements within the target method as shown below (Exhibit #10).

由于我们向目标实体注入了模拟对象,因此我们可以轻松地用每一行代码中期望的更多模拟对象来控制目标代码。 例如,请参阅连接管理器实例应该在目标方法的第二行上创建并返回IDbConnection实例,让我们要求连接管理器模拟在此处返回我们自己的模拟连接。 因此,让我们添加一组行为,我们的主要模拟对象应在目标方法内的每个语句上执行,如下所示(图10)。

Exhibit #10

展览#10

// First statement when connectionstring is attempted to retrieve
configurationManagerFacade.GetConnectionString("defaultConnection").Returns(connectionString);
// second statement to create a connection
var mockConnection = Substitute.For<IDbConnection>();
connectionManager.CreateConnection(connectionString).Returns(mockConnection);
// third statement to create a command
var mockCommand = Substitute.For<IDbCommand>();
connectionManager.CreateCommand(storedProcedureName).Returns(mockCommand);
// seventh statement when execute reader is invoked
var reader = Substitute.For<IDataReader>();
mockCommand.ExecuteReader().Returns(reader); 

Now invoke our target method from the test method and pass input parameters for the function. Here, since one of the argument is a function that is supposed to get the data reader during execution, get that on a variable which in turn can be asserted to ensure that expectation is met.

现在从测试方法中调用我们的目标方法,并传递函数的输入参数。 在这里,由于自变量之一是应该在执行期间获取数据读取器的函数,因此可以在变量上获取该参数,然后可以将该变量断言以确保满足期望。

Exhibit #11

展览#11

//Act
IDataReader dataReader = null;
var storedProcedureName = "TestStoredProcedure";
var result = repository.Retrieve(storedProcedureName, (dr) => 
{
    dataReader = dr;
    return true;
}); 

Once we acted upon the target method, it is the time we should see all our expectations were met by asserting against our mock objects. Assertions are not limited to output values, but method invocations we expect on each mock objects and even the order of its executions can be asserted. A very strict assertion helps to keep our code strictly protected from unintended changes in future (see Exhibit #12).

一旦对目标方法采取了行动,就应该通过对我们的模拟对象进行断言来满足所有期望。 断言不限于输出值,而是我们可以期望的对每个模拟对象的方法调用,甚至可以断言其执行顺序。 一个非常严格的声明有助于使我们的代码受到严格保护,以防止将来发生意外更改(请参见图表12)。

Exhibit #12

展览#12

// below calls are received and received in order of their appearance
Received.InOrder(() =>
{
    // the supplied connection is assigned to command object
    mockCommand.Connection = mockConnection;
    // a connection open was invoked
    mockConnection.Open();
    // reader was created
    mockCommand.ExecuteReader();
    // connection was closed after reader is created
    mockConnection.Close();
});
// trace was not written since success=true
logger.DidNotReceive().Trace(Arg.Any<string>());
// reader returned from command is the same object that is supplied to Func<T>
Assert.AreEqual(reader, dataReader);
// Finally true returned from Func<T> is attained in return
Assert.IsTrue(result); 

Finally, our test method will be as shown in Exhibit #13

最后,我们的测试方法将如图表13所示。

Exhibit #13

展览#13

[Test]
public void Retrieve_CanRetrieve()
{
    //Arrange
    var connectionManager = Substitute.For<IConnectionManager>();
    var configurationManagerFacade = Substitute.For<IConfigurationManagerFacade>();
    var logger = Substitute.For<ILogger>();
    var repository = new Repository(connectionManager, configurationManagerFacade, logger);


    var connectionString = "TestConnectionString";
    var storedProcedureName = "TestStoredProcedure";
    configurationManagerFacade.GetConnectionString("defaultConnection").Returns(connectionString);
    var mockConnection = Substitute.For<IDbConnection>();
    connectionManager.CreateConnection(connectionString).Returns(mockConnection);
    var mockCommand = Substitute.For<IDbCommand>();
    connectionManager.CreateCommand(storedProcedureName).Returns(mockCommand);
    var reader = Substitute.For<IDataReader>();
    mockCommand.ExecuteReader().Returns(reader);


    //Act
    IDataReader dataReader = null;
    var result = repository.Retrieve(storedProcedureName, (dr) => 
    {
        dataReader = dr;
        return true;
    });


    //Assert
    Received.InOrder(() =>
    {
        mockCommand.Connection = mockConnection;
        mockConnection.Open();
        mockCommand.ExecuteReader();
        mockConnection.Close();
    });


    logger.DidNotReceive().Trace(Arg.Any<string>());
    Assert.AreEqual(reader, dataReader);
    Assert.IsTrue(result);
} 

In this test, we ensure all the statements are executed in the target method except the Trace. Since it is very important to cover 100% of our code unity test, we should create another test method in turn sets the success variable to a false and thus, making the condition to log the statement. 

在此测试中,我们确保除Trace之外的所有语句均在目标方法中执行。 由于覆盖100%的代码统一测试非常重要,因此我们应该创建另一种测试方法,依次将成功变量设置为false,从而使条件能够记录该语句。

How I check my unit test coverage?

如何检查单元测试范围?

There are free tools available to check the code coverage locally on a visual studio IDE and I use AxoCover to see the test coverage. I have seen architects happy with 80% or more of a coverage, in most cases I could hit 100% by following the practices we have discussed earlier in this article (especially isolation techniques).

有免费的工具可以在Visual Studio IDE上本地检查代码覆盖率,我使用AxoCover查看测试覆盖率。 我已经看到架构师对覆盖率的80%或更多满意,在大多数情况下,按照我们在本文前面讨论的实践(尤其是隔离技术),我可以达到100%。

How do I test my unit tests?

如何测试单元测试?

Here is the most important part before we could conclude writing tests against a target method. I personally would make changes to my code under test and see my tests fails on any impacting change that I made. In our example as in Exhibit #8, my test fails if I comment out any one line of code or I change assignment of any of those variables, by not calling a function or even I change the order of invocations. Try these on your target method and ensure any of these action is failing your tests.

在结束针对目标方法编写测试之前,这是最重要的部分。 我个人将对测试中的代码进行更改,并看到我所做的任何有影响的更改都使测试失败。 在示例#8中,如果我注释掉任意一行代码,或者通过不调用函数甚至更改调用顺序来更改这些变量的赋值,则测试将失败。 在您的目标方法上尝试这些,并确保任何这些操作均未通过测试。

I bet, this way of writing strict unit tests keeps our code clean and green forever. Thanks!

我敢打赌,这种编写严格的单元测试的方法可以使我们的代码永远保持绿色清洁。 谢谢!

翻译自: https://www.experts-exchange.com/articles/31199/Solve-Unit-Testing-challenges-in-C.html

c# 单元测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值