单元测试的依赖性

Tha*_*ton 2 php testing dependencies phpunit unit-testing

目前,我正在尝试首次将单元测试应用于项目.出现了两个问题:

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

  2. 您与SUT依赖的模拟物体相距多远?在下面的代码中,'Router'取决于'Route',它取决于'RouteParameter'.嘲笑还是不嘲笑?

下面的代码是测试我的'Router'对象,它接受路由Router::addRoute($route)并通过URL路由URL Router::route($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');
    }
}
Run Code Online (Sandbox Code Playgroud)

Wou*_*ort 5

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

单元测试应该以这样的方式构建:当它失败时,它会立即指向代码中的特定区域.良好的单元测试将减少您花在调试上的时间.如果测试依赖于彼此,您将失去此优势,因为您无法分辨代码中的哪个错误导致测试失败.而且,这是一场维护噩梦.如果您的"共享测试"中的某些内容发生了变化,那么您将不得不更改所有依赖的测试.

在这里你可以找到一些关于如何解决相互作用的测试问题的良好指导(整个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 
        // ....
    }
}
Run Code Online (Sandbox Code Playgroud)

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

但是,以下内容将允许您模拟hardwareClient和satelliteSyncService.

public AlarmClock(IHardwareClient hardwareClient, ISatelliteSyncService satelliteSyncService)
{
    SatelliteSyncService = satelliteSyncService;
    HardwareClient = hardwareClient;
}
Run Code Online (Sandbox Code Playgroud)

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

所以,你应该在多大程度上嘲笑.你应该模拟测试所依赖的类的所有内容.通过这种方式,您可以完全隔离地测试您的课程.并且您可以控制依赖关系的结果,这样您就可以确保您的SUT将通过所有代码路径.

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

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

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