【问题标题】:Dependencies in Unit Testing单元测试中的依赖
【发布时间】:2012-02-13 01:32:43
【问题描述】:

目前,我第一次尝试将单元测试应用于项目。出现了两个问题:

  1. 如果多个测试相互依赖是不好的做法吗?在下面的代码中,几个测试需要其他测试的结果是肯定的,这是一般的最佳做法吗?

  2. SUT 所依赖的模拟对象能走多远? 在下面的代码中,“Router”依赖于“Route”,而“Route”又依赖于“RouteParameter”。模仿还是不模仿?

下面的代码用于测试我的“Router”对象,该对象通过Router::addRoute($route) 接受路由并通过Router::route($url) 路由URL。

class RouterTest extends PHPUnit_Framework_TestCase {
    protected function createSimpleRoute() {
        $route = new \TNT\Core\Models\Route();
        $route->alias = 'alias';
        $route->route = 'route';
        $route->parameters = array();

        return $route;
    }

    protected function createAlphanumericRoute() {
        $route = new \TNT\Core\Models\Route();
        $route->alias = 'alias';
        $route->route = 'test/[id]-[name]';

        $parameterId = new \TNT\Core\Models\RouteParameter();
        $parameterId->alias = 'id';
        $parameterId->expression = '[0-9]+';

        $parameterName = new \TNT\Core\Models\RouteParameter();
        $parameterName->alias = 'name';
        $parameterName->expression = '[a-zA-Z0-9-]+';

        $route->parameters = array($parameterId, $parameterName);

        return $route;
    }

    public function testFilledAfterAdd() {
        $router = new \TNT\Core\Helpers\Router();

        $router->addRoute($this->createSimpleRoute());

        $routes = $router->getAllRoutes();

        $this->assertEquals(count($routes), 1);

        $this->assertEquals($routes[0], $this->createSimpleRoute());

        return $router;
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testOverwriteExistingRoute($router) {
        $router->addRoute(clone $this->createSimpleRoute());

        $this->assertEquals(count($router->getAllRoutes()), 1);
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testSimpleRouting($router) {
        $this->assertEquals($router->route('route'), $this->createSimpleRoute());
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testAlphanumericRouting($router) {
        $router->addRoute($this->createAlphanumericRoute());

        $found = $router->route('test/123-Blaat-and-Blaat');

        $data = array('id' => 123, 'name' => 'Blaat-and-Blaat');

        $this->assertEquals($found->data, $data);
    }

    /**
    * @expectedException TNT\Core\Exceptions\RouteNotFoundException
    */
    public function testNonExistingRoute() {
        $router = new \TNT\Core\Helpers\Router();

        $router->route('not_a_route');
    }
}

【问题讨论】:

    标签: php unit-testing testing dependencies phpunit


    【解决方案1】:

    1) 是的,如果测试相互依赖,这绝对是一种不好的做法。

    单元测试的构造方式应该是,当它失败时,它会立即指向代码中的特定区域。好的单元测试会减少你花在调试上的时间。如果测试相互依赖,您将失去此优势,因为您无法分辨代码中的哪个错误导致测试失败。此外,这是维护的噩梦。如果您的“共享测试”发生变化,那么您将不得不更改所有依赖的测试。

    Here 你可以找到一些关于如何解决交互测试问题的很好的指导(整本 xUnit 测试模式书是必读的!)

    2) 单元测试是关于测试尽可能小的东西。

    假设你有一个闹钟(C# 代码):

    public class AlarmClock
    {
        public AlarmClock()
        {
            SatelliteSyncService = new SatelliteSyncService();
            HardwareClient = new HardwareClient();
        }
    
        public void Execute()
        {
            HardwareClient.DisplayTime = SatelliteSyncService.GetTime();
    
            // Check for active alarms 
            // ....
        }
    }
    

    这是不可测试的。您将需要一个真正的卫星连接和一个硬件客户端来检查是否设置了正确的时间。

    然而,下面将让您模拟 hardwareClient 和 satelliteSyncService。

    public AlarmClock(IHardwareClient hardwareClient, ISatelliteSyncService satelliteSyncService)
    {
        SatelliteSyncService = satelliteSyncService;
        HardwareClient = hardwareClient;
    }
    

    但是,您不应该模拟您实际测试的对象(听起来合乎逻辑,但有时我会看到它发生)。

    那么,你应该在嘲笑方面走多远。您应该模拟您的测试所依赖的类的所有内容。通过这种方式,您可以完全隔离地测试您的课程。而且您可以控制依赖项的结果,从而确保您的 SUT 将通过所有代码路径。

    例如,让SatelliteSyncService 抛出一个异常,让它返回一个无效的时间,当然让它返回正确的时间,然后在特定的时刻,这样你就可以测试你的闹钟是否在正确的时刻被激活。

    用于创建您的 Route 测试数据。考虑使用Builder Pattern. 这将帮助您仅设置测试成功所需的内容。这将使您的测试更具表现力,并且更易于他人阅读。它还会降低您的测试维护,因为您的依赖项更少。

    我写了一篇关于单元测试的blog post,它扩展了这里提到的想法。它使用 C# 来解释概念,但它适用于所有语言。

    【讨论】:

    • +1,关于测试和模拟还有很多话要说,但这是一个很好的介绍
    【解决方案2】:

    您的示例没有显示相互依赖的测试,而是显示了使用相关类 Route 和 RouteParameter 的类 Router 的单个测试。由于这些其他人是数据持有者,因此我在 Router 测试中使用它们没有问题。

    正如 Wouter de Kort 指出的那样,在测试时使用模拟对象非常有帮助,但请记住,存在权衡取舍。测试可能更难阅读和维护,而且由于测试代码没有自己的测试,任何复杂性都是一种风险。

    【讨论】:

    • 在我的示例中,一些测试相互依赖。 testOverwriteExistingRoute 例如取决于 testFilledAfterAdd。我解释错了吗,或者这只是不被认为是真正的“依赖”?测试依赖是否意味着多个测试类相互使用?
    • 测试依赖通常使用 @depends X 注释来表示,这意味着“测试 X 必须通过,否则测试 Y 将被跳过”,并且测试 X 返回的任何内容都将传递给测试 Y。虽然这往往会使测试更加复杂,如果谨慎使用它会很有用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-27
    • 2021-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多