我读过各种关于测试中模仿和存根的文章,包括Martin Fowler的《Mocks Aren't Stubs》,但我仍然不理解其中的区别。


当前回答

参见下面使用c#和Moq框架的mock和存根的例子。Moq对于存根没有特殊的关键字,但是您也可以使用Mock对象来创建存根。

namespace UnitTestProject2
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Moq;
    [TestClass]
    public class UnitTest1
    {
        /// <summary>
        /// Test using Mock to Verify that GetNameWithPrefix method calls Repository GetName method "once" when Id is greater than Zero
        /// </summary>
        [TestMethod]
        public void GetNameWithPrefix_IdIsTwelve_GetNameCalledOnce()
        {
            // Arrange 
            var mockEntityRepository = new Mock<IEntityRepository>();
            mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));

            var entity = new EntityClass(mockEntityRepository.Object);
            // Act 
            var name = entity.GetNameWithPrefix(12);
            // Assert
            mockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Once);
        }
        /// <summary>
        /// Test using Mock to Verify that GetNameWithPrefix method doesn't call Repository GetName method when Id is Zero
        /// </summary>
        [TestMethod]
        public void GetNameWithPrefix_IdIsZero_GetNameNeverCalled()
        {
            // Arrange 
            var mockEntityRepository = new Mock<IEntityRepository>();
            mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));
            var entity = new EntityClass(mockEntityRepository.Object);
            // Act 
            var name = entity.GetNameWithPrefix(0);
            // Assert
            mockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Never);
        }
        /// <summary>
        /// Test using Stub to Verify that GetNameWithPrefix method returns Name with a Prefix
        /// </summary>
        [TestMethod]
        public void GetNameWithPrefix_IdIsTwelve_ReturnsNameWithPrefix()
        {
            // Arrange 
            var stubEntityRepository = new Mock<IEntityRepository>();
            stubEntityRepository.Setup(m => m.GetName(It.IsAny<int>()))
                .Returns("Stub");
            const string EXPECTED_NAME_WITH_PREFIX = "Mr. Stub";
            var entity = new EntityClass(stubEntityRepository.Object);
            // Act 
            var name = entity.GetNameWithPrefix(12);
            // Assert
            Assert.AreEqual(EXPECTED_NAME_WITH_PREFIX, name);
        }
    }
    public class EntityClass
    {
        private IEntityRepository _entityRepository;
        public EntityClass(IEntityRepository entityRepository)
        {
            this._entityRepository = entityRepository;
        }
        public string Name { get; set; }
        public string GetNameWithPrefix(int id)
        {
            string name = string.Empty;
            if (id > 0)
            {
                name = this._entityRepository.GetName(id);
            }
            return "Mr. " + name;
        }
    }
    public interface IEntityRepository
    {
        string GetName(int id);
    }
    public class EntityRepository:IEntityRepository
    {
        public string GetName(int id)
        {
            // Code to connect to DB and get name based on Id
            return "NameFromDb";
        }
    }
}

其他回答

Stub帮助我们运行测试。怎么做?它给出了有助于运行测试的值。这些值本身不是真实的,我们创建这些值只是为了运行测试。例如,我们创建一个HashMap来提供与数据库表中的值相似的值。因此,我们不直接与数据库交互,而是与Hashmap交互。

Mock是一个运行测试的伪对象。我们在这里输入assert。

Stub

存根是用来伪造具有预先编程行为的方法的对象。为了避免不必要的副作用(例如,存根可能会进行一个虚假的获取调用,返回预先编程的响应,而不实际向服务器发出请求),您可能会使用这个方法来代替现有的方法。

Mock

mock是用来模拟具有预编程行为和预编程期望的方法的对象。如果这些期望没有得到满足,那么mock将导致测试失败(例如,mock可以进行一个虚假的获取调用,返回预先编程的响应,而不实际向服务器发出请求,例如,第一个参数是“http://localhost:3008/”,否则测试将失败。)

区别

与模拟不同,存根没有预先编程的可能导致测试失败的期望。

mock既是技术对象,也是功能对象。

这个模拟是技术性的。它确实是由一个模拟库(EasyMock、JMockit和最近的Mockito都以这些库而闻名)创建的,这要归功于字节代码生成。 生成模拟实现的方式是,我们可以在调用方法时将其设置为返回特定值,但也可以执行其他一些事情,例如验证模拟方法是否使用某些特定参数(严格检查)或任何参数(不严格检查)调用。

实例化一个mock:

@Mock Foo fooMock

记录行为:

when(fooMock.hello()).thenReturn("hello you!");

验证调用:

verify(fooMock).hello()

这些显然不是实例化/重写Foo类/行为的自然方式。这就是为什么我提到技术方面。

但是mock也是功能性的,因为它是我们需要从SUT中隔离出来的类的实例。有了记录在上面的行为,我们就可以在SUT中使用它就像使用存根一样。


存根只是一个函数对象:它是我们需要从SUT中隔离出来的类的实例,仅此而已。 这意味着在单元测试期间需要的存根类和所有行为fixture都必须显式地定义。 例如,存根hello()将需要子类化Foo类(或实现它拥有的接口)并重写hello():

public class HelloStub extends Hello{    
  public String hello { 
      return "hello you!"; 
  }
}

如果另一个测试场景需要另一个返回值,我们可能需要定义一个通用的方法来设置返回值:

public class HelloStub extends Hello{    
  public HelloStub(String helloReturn){
       this.helloReturn = helloReturn;
  }
  public String hello { 
      return helloReturn; 
  }
}

其他场景:如果我有一个副作用方法(没有返回),并且我要检查该方法是否被调用,我可能应该在存根类中添加一个布尔值或计数器来计算该方法被调用的次数。


结论

存根通常需要为单元测试编写大量开销/代码。mock因为提供了开箱即用的记录/验证功能而阻止了什么。 这就是为什么现在随着优秀的模拟库的出现,存根方法很少在实践中使用。


关于Martin Fowler文章:当我使用mock和避免存根时,我不认为我是一个“嘲笑主义者”程序员。 但是当真正需要时我使用mock(讨厌的依赖关系),当我测试带有依赖关系的类时,我喜欢测试切片和迷你集成测试,而mock将是一种开销。

下面是对每一个的描述,然后是真实世界的样本。

Dummy - just bogus values to satisfy the API. Example: If you're testing a method of a class which requires many mandatory parameters in a constructor which have no effect on your test, then you may create dummy objects for the purpose of creating new instances of a class. Fake - create a test implementation of a class which may have a dependency on some external infrastructure. (It's good practice that your unit test does NOT actually interact with external infrastructure.) Example: Create fake implementation for accessing a database, replace it with in-memory collection. Stub - override methods to return hard-coded values, also referred to as state-based. Example: Your test class depends on a method Calculate() taking 5 minutes to complete. Rather than wait for 5 minutes you can replace its real implementation with stub that returns hard-coded values; taking only a small fraction of the time. Mock - very similar to Stub but interaction-based rather than state-based. This means you don't expect from Mock to return some value, but to assume that specific order of method calls are made. Example: You're testing a user registration class. After calling Save, it should call SendConfirmationEmail.

存根和Mock实际上是Mock的子类型,两者都交换了实际实现和测试实现,但出于不同的、特定的原因。

我认为他们之间最重要的区别是他们的意图。

让我试着用WHY stub和WHY mock来解释它

假设我正在为我的mac twitter客户端的公共时间轴控制器编写测试代码

下面是测试示例代码

twitter_api.stub(:public_timeline).and_return(public_timeline_array)
client_ui.should_receive(:insert_timeline_above).with(public_timeline_array)
controller.refresh_public_timeline

STUB:到twitter API的网络连接非常慢,这使得我的测试很慢。我知道它将返回时间轴,所以我制作了一个模拟HTTP twitter API的存根,这样我的测试将非常快地运行它,即使我离线也可以运行测试。 MOCK:我还没有写任何我的UI方法,我不确定我需要为我的UI对象写什么方法。我希望通过编写测试代码了解我的控制器如何与我的ui对象协作。

通过编写mock,您可以通过验证期望是否满足来发现对象的协作关系,而stub仅模拟对象的行为。

如果您想了解更多关于模拟的知识,我建议您阅读这篇文章:http://jmock.org/oopsla2004.pdf