65.9K
CodeProject 正在变化。 阅读更多。
Home

模拟入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (20投票s)

2008年10月23日

CPOL

3分钟阅读

viewsIcon

134169

downloadIcon

1459

本文将解释模拟及其在单元测试中带来的各种好处。

引言

模拟是单元测试不可或缺的一部分。虽然你可以在不使用模拟的情况下运行单元测试,但这会极大地降低单元测试的执行速度,并且会依赖于外部资源。本文将解释模拟及其在单元测试中带来的各种好处。

现实世界中的例子

我们将专注于一个现实世界的例子,以便你更好地理解模拟。假设我们正在开发一个银行软件,它从某个数据库中提取客户余额信息。该软件还会向外部网络服务发出身份验证请求。身份验证过程很慢,因为它涉及许多不同的步骤,包括发现网络服务、序列化输入和输出参数等。

我们想测试返回的客户余额是否正确。

这是带有`Authenticate`方法的网络服务

public class AverageJoeBankService : IAverageJoeBankService
    {
        public bool Authenticate(string userName, string password)
        {
            // This is just simulate the time taken for the web service to authenticate! 
            System.Threading.Thread.Sleep(5000); 
            return true; 
        }     
    }

如你所见,我们只是模拟了`Authenticate`方法。我们假设验证用户至少需要5秒钟。当然,在实际应用中,你的`Authenticate`方法将与真实的网络服务通信。

这是`AccountBalanceService`方法

public class AccountBalanceService
    {
        private IAverageJoeBankService _averageJoeService;

        public AccountBalanceService(IAverageJoeBankService averageJoeService)
        {
            _averageJoeService = averageJoeService;
        }

        public double GetAccountBalanceByUser(User user)
        {       
            // the authenticate method below takes too much time! 
            bool isAuthenticated = _averageJoeService.Authenticate(user.UserName,
                user.Password);

            if (!isAuthenticated)
                throw new SecurityException("User is not authenticated"); 
            
            // access database using username and get the balance 

            return 100;      
        }
    }

这是我们第一次尝试编写单元测试

        [Test]
        public void should_be_able_to_get_the_balance_successfully_without_using_mock_objects()
        {
            User user = new User();
            user.UserName = "johndoe";
            user.Password = "johnpassword";

            _accountBalanceService = new AccountBalanceService(new AverageJoeBankService());

            Assert.AreEqual(100, _accountBalanceService.GetAccountBalanceByUser(user)); 
        }

在上面的单元测试中,我们只是使用了`AccountBalanceService`类的具体实现,它会触发实际的身份验证方法。

运行测试时,你会得到以下结果

IntroductionToMocking_Image001small.PNG

测试通过了,你脸上露出了灿烂的笑容。别高兴得太早!看看运行测试所花费的时间:6.69 秒。对于单个测试来说,这时间太长了。除了时间问题外,测试还存在其他问题。测试依赖于`AverageJoeBankService`。如果网络服务不可用,则测试将失败。单元测试应该是独立的,并且应该运行速度很快。让我们通过引入模拟对象来加快测试速度。

哦,等等!我们还没有解释模拟对象在单元测试中的含义。模拟对象就像真实对象,但它们什么也不做。我们知道我们刚刚给你的定义有点疯狂,但你很快就会明白我们的意思。

这是使用模拟对象的单元测试

private AccountBalanceService _accountBalanceService;
private MockRepository _mocks;

[SetUp]
        public void initialize()
        {
            _mocks = new MockRepository();            
        }

[Test]
        public void should_be_able_to_get_the_balance_successfully() 
        {
            User user = new User();
            user.UserName = "JohnDoe";
            user.Password = "JohnPassword";

            var averageJoeService = _mocks.DynamicMock<IAverageJoeBankService>();

            _accountBalanceService = new AccountBalanceService(averageJoeService);

            using (_mocks.Record())
            {
                SetupResult.For(averageJoeService.Authenticate(null,
                    null)).IgnoreArguments().Return(true); 

            }

            using (_mocks.Playback())
            {
                Assert.AreEqual(100,_accountBalanceService.GetAccountBalanceByUser(user)); 
            }          
        }

首先,我们创建了`MockRepository`,它是Rhino Mocks框架的一部分。你可能会说“Rhino Mocks是什么鬼?”Rhino mock是一个模拟框架,它为你提供不同的模拟功能以及模拟对象的方法。还有其他几个模拟框架,包括NMock、NMock2、TypeMock、MoQ。

无论如何,在上面的单元测试中,我们使用以下代码创建了一个模拟对象

var averageJoeService = _mocks.DynamicMock<IAverageJoeBankService>();
_accountBalanceService = new AccountBalanceService(averageJoeService);

将模拟的`AverageJoeService`(不是真正的服务)传递给`AccountBalanceService`后,我们设置了我们的期望。

using (_mocks.Record())
    {
        SetupResult.For(averageJoeService.Authenticate(null,
            null)).IgnoreArguments().Return(true); 

    }

这意味着当`averageJoeService.Authenticate`方法被触发时,返回true,这样我就可以继续执行了。你还可以看到我们并不关心传入的参数,这就是为什么我们使用`IgnoreArguments`。

下一部分是回放,这是将触发期望并产生预期结果的代码。这是回放部分

using (_mocks.Playback())
    {
        Assert.AreEqual(100,_accountBalanceService.GetAccountBalanceByUser(user)); 
    }

这是单元测试的输出

IntroductionToMocking_Image002small.PNG

如你所见,现在单元测试只需要1.81秒。测试不仅更快,而且现在也不依赖于`AverageJoeBankService`。这意味着即使网络服务宕机,你也能运行上面的测试。

结论

在本文中,我们介绍了模拟背后的概念。模拟通过消除外部依赖性来帮助改进单元测试,从而产生更好、更快、更独立的单元测试。

希望你喜欢这篇文章,编程愉快!

注意:我们还为此主题创建了一个屏幕录制视频,你可以使用以下链接观看:屏幕录制:模拟入门

© . All rights reserved.