单元测试中的依赖关系

发布于 2024-12-27 09:52:27 字数 2716 浏览 6 评论 0原文

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

  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');
    }
}

Currently, I'm trying to apply unit testing to a project for the first time. Two questions arose:

  1. Is it bad practice if multiple tests depend on each other? In the code below, several test need the outcome of the other tests to be positive, is this general best practice?

  2. How far do you go with mocking objects the SUT depends on? In the code below, the 'Router' depends on 'Route', which depends on 'RouteParameter'. To mock, or not to mock?

The code below is to test my 'Router' object, which accepts routes via Router::addRoute($route) and routes an URL via 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');
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

这个俗人 2025-01-03 09:52:27

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 
        // ....
    }
}

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

然而,以下内容将让您模拟 hardwareClient 和 SatelliteSyncService。

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

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

那么,你应该嘲笑到什么程度呢?您应该模拟您的测试所依赖的类的所有内容。通过这种方式,您可以完全隔离地测试您的类。您可以控制依赖项的结果,以便确保您的 SUT 将通过所有代码路径。

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

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

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

1) Yes it is definitely a bad practice if tests depend on each other.

A Unit Test should be constructed in such a way that when it fails it immediately points to a specific area in your code. Good Unit Tests will reduce the time you spend debugging. If Tests depend on each other you will loose this benefit because you can't tell which error in your code made the test fail. Also, it's a maintenance nightmare. What if something changes in your 'shared test', then you would have to change all depended tests.

Here you can find some good guidance on how to solve the interacting test problems (The whole xUnit Test pattern book is a must read!)

2) Unit Testing is about testing the smallest thing possible.

Let's say you have an alarm clock (C# code):

public class AlarmClock
{
    public AlarmClock()
    {
        SatelliteSyncService = new SatelliteSyncService();
        HardwareClient = new HardwareClient();
    }

    public void Execute()
    {
        HardwareClient.DisplayTime = SatelliteSyncService.GetTime();

        // Check for active alarms 
        // ....
    }
}

This is not testable. You will need a real satellite connection and a hardware client to check if the right time is set.

The following however will let you mock both hardwareClient and satelliteSyncService.

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

You should however never mock an object that you are actually testing (sounds logical but sometimes I see it happening).

So, how far should you go with mocking. You should mock everything that the class your testing depends on. In such a way you can test your class in complete isolation. And you can control the outcome of your dependencies so you can make sure that your SUT will go trough all code paths.

For example, let SatelliteSyncService throw an exception, let it return an invalid time, and off course let it return the correct time and then at specific moments so you can test if your alarm is activated on the right moment.

For creating your Route test data. Consider using the Builder Pattern. That will help you in only setting what is required for your test to succeed. It will make your tests more expressive and easier to read for others. It will also lower your test maintenance because you have fewer dependencies.

I wrote a blog post about unit testing that expands on the ideas mentioned here. It uses C# to explain the concepts but it applies to all languages.

脱离于你 2025-01-03 09:52:27

您的示例并未显示相互依赖的测试,而是显示了使用相关类 RouteRouteParameter 的类 Router 的单个测试。由于其他这些都是数据持有者,因此我在 Router 测试中使用它们不会有任何问题。

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

Your example doesn't show tests depending on each other but rather a single test for class Router that makes use of related classes Route and RouteParameter. Since these others are data holders, I would have no problem with using them in the Router tests.

Using mock objects can be very helpful when testing as Wouter de Kort pointed out, but keep in mind that there are trade-offs. The tests can be harder to read and maintain, and since test code doesn't have its own tests, any complexity is a risk.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文