unit-testing开始进行单元测试


备注

单元测试描述了独立于它们所属系统的各个代码单元的测试过程。构成单位的内容因系统而异,从单个方法到一组密切相关的类或模块。

在必要时使用测试双精度将单元与其依赖关系隔离并设置为已知状态。然后针对预期行为测试其对刺激(方法调用,事件,模拟数据)的反应行为。

整个系统的单元测试可以使用自定义编写的测试工具来完成,但是已经编写了许多测试框架来帮助简化流程并处理大量的管道,重复和平凡的任务。这允许开发人员专注于他们想要测试的内容。

当项目有足够的单元测试时,通过最终验证一切都像以前一样工作,可以轻松完成添加新功能或执行代码重构的任何修改。

代码覆盖率 ,通常表示为百分比,是用于显示单元测试涵盖系统中代码量的典型度量标准;请注意,关于这应该有多高,并没有严格的规则,但人们普遍认为越高越好。

测试驱动开发(TDD)是一个原则,规定开发人员应该通过编写失败的单元测试来开始编码,然后才能编写使测试通过的生产代码。在练习TDD时,可以说测试本身是所创建代码的第一个消费者;因此,它们有助于审计和驱动代码的设计,使其尽可能简单易用。

版本

单元测试是一个没有版本号的概念。

带参数的XUnit测试

using Xunit;

public class SimpleCalculatorTests
{
    [Theory]
    [InlineData(0, 0, 0, true)]
    [InlineData(1, 1, 2, true)]
    [InlineData(1, 1, 3, false)]
    public void Add_PassMultipleParameters_VerifyExpected(
        int inputX, int inputY, int expected, bool isExpectedCorrect)
    {
        // Arrange
        var sut = new SimpleCalculator();

        // Act
        var actual = sut.Add(inputX, inputY);

        // Assert
        if (isExpectedCorrect)
        {
            Assert.Equal(expected, actual);
        }
        else
        {
            Assert.NotEqual(expected, actual);
        }
    }
}

public class SimpleCalculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }
}
 

一个基本的python单元测试

import unittest

def addition(*args):
    """ add two or more summands and return the sum """

    if len(args) < 2:
        raise ValueError, 'at least two summands are needed'
    
    for ii in args: 
        if not isinstance(ii, (int, long, float, complex )):
            raise TypeError

    # use build in function to do the job
    return sum(args) 
 

现在测试部分:

class Test_SystemUnderTest(unittest.TestCase):

    def test_addition(self):
        """test addition function"""

        # use only one summand - raise an error 
        with self.assertRaisesRegexp(ValueError, 'at least two summands'):
            addition(1)
        
        # use None - raise an error
        with self.assertRaises(TypeError):
            addition(1, None)
        
        # use ints and floats 
        self.assertEqual(addition(1, 1.), 2)

        # use complex numbers
        self.assertEqual(addition(1, 1., 1+2j), 3+2j)

if __name__ == '__main__':
    unittest.main()
 

基本单元测试

最简单的是,单元测试包括三个阶段:

  • 准备测试环境
  • 执行要测试的代码
  • 验证与观察到的行为匹配的预期行为

这三个阶段通常被称为'Arrange-Act-Assert',或'Given-When-Then'。

下面是使用NUnit框架的C#中的示例。

[TestFixture]
public CalculatorTest
{
   [Test]
   public void Add_PassSevenAndThree_ExpectTen()
   {
       // Arrange - setup environment
       var systemUnderTest = new Calculator();         

       // Act - Call system under test
       var calculatedSum = systemUnderTest.Add(7, 3);  
       
       // Assert - Validate expected result
       Assert.AreEqual(10, calculatedSum);             
  }
}
 

必要时,可选的第四个清理阶段进行整理。

带间谍的单元测试(交互测试)

经典单元测试测试状态 ,但是通过状态正确测试行为依赖于其他类的方法是不可能的。我们通过交互测试来测试这些方法,这些测试验证被测系统正确调用其协作者。由于合作者有自己的单元测试,这就足够了,实际上是对测试方法的实际责任的更好测试。我们不测试此方法在给定输入的情况下返回特定结果,而是测试其正确调用其协作者。

// Test that squareOfDouble invokes square() with the doubled value

let systemUnderTest = new Calculator()          // Arrange - setup environment
let square = spy()
systemUnderTest.setSquare(square)               //   inject a spy

let actual = systemUnderTest.squareOfDouble(3)  // Act - Call system under test

assert(square.calledWith(6))                    // Assert - Validate expected interaction
 

具有存根依赖性的单元测试

好的单元测试是独立的,但代码通常具有依赖性。我们使用各种测试双精度来删除测试的依赖性。最简单的测试双打之一是存根。这是一个带有硬编码返回值的函数,代替现实世界的依赖。

// Test that oneDayFromNow returns a value 24*60*60 seconds later than current time

let systemUnderTest = new FortuneTeller()       // Arrange - setup environment
systemUnderTest.setNow(() => {return 10000})    //   inject a stub which will 
                                                //   return 10000 as the result

let actual = systemUnderTest.oneDayFromNow()    // Act - Call system under test

assert.equals(actual, 10000 + 24 * 60 * 60)     // Assert - Validate expected result
 

在生产代码中, oneDayFromNow 会调用Date.now(),但这会导致不一致和不可靠的测试。所以在这里我们将其删除。

简单的Java + JUnit测试

JUnit是用于测试Java代码的领先测试框架。

被测试的课程模拟了一个简单的银行账户,当您透支时收取罚款。

public class BankAccount {
    private int balance;

    public BankAccount(int i){
        balance = i;
    }

    public BankAccount(){
        balance = 0;
    }

    public int getBalance(){
        return balance;
    }

    public void deposit(int i){
        balance += i;
    }

    public void withdraw(int i){
        balance -= i;
        if (balance < 0){
            balance -= 10; // penalty if overdrawn
        }
    }
}
 

此测试类验证某些BankAccount 公共方法的行为。

import org.junit.Test;
import static org.junit.Assert.*;

// Class that tests
public class BankAccountTest{

    BankAccount acc;

    @Before                        // This will run **before** EACH @Test
    public void setUptestDepositUpdatesBalance(){
        acc = new BankAccount(100);  
    } 

    @After                        // This Will run **after** EACH @Test
    public void tearDown(){
    // clean up code
    }

    @Test
    public void testDeposit(){
       // no need to instantiate a new BankAccount(), @Before does it for us

        acc.deposit(100);

        assertEquals(acc.getBalance(),200); 
    }

    @Test
    public void testWithdrawUpdatesBalance(){    
        acc.withdraw(30);

        assertEquals(acc.getBalance(),70); // pass
    }

    @Test
    public void testWithdrawAppliesPenaltyWhenOverdrawn(){

        acc.withdraw(120);

        assertEquals(acc.getBalance(),-30);
    }
}
 

使用NUnit和C#的参数单元测试

using NUnit.Framework;

namespace MyModuleTests 
{
    [TestFixture]
    public class MyClassTests
    {
        [TestCase(1, "Hello", true)]
        [TestCase(2, "bye", false)]
        public void MyMethod_WhenCalledWithParameters_ReturnsExpected(int param1, string param2, bool expected)
        {
        //Arrange
        var foo = new MyClass(param1);

        //Act
        var result = foo.MyMethod(param2);

        //Assert
        Assert.AreEqual(expected, result);
        }
    }
}