目前,我正在尝试首次将单元测试应用于项目。出现了两个问题:
如果多个测试相互依赖,这是不好的做法吗?在下面的代码中,一些测试需要其他测试的结果是积极的,这是一般的最佳实践吗? / p>
你对SUT依赖的模拟对象走了多远?在下面的代码中,'Router'取决于'Route',它取决于'RouteParameter'。嘲笑还是不嘲笑?
以下代码用于测试我的“路由器”对象,该对象通过Router::addRoute($route)
接受路由,并通过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');
}
}
答案 0 :(得分:5)
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
抛出一个异常,让它返回一个无效的时间,当然让它返回正确的时间,然后在特定时刻让你可以测试你的警报是否在正确的时刻被激活
用于创建路线测试数据。考虑使用Builder Pattern.这将帮助您仅设置测试成功所需的内容。它将使您的测试更具表现力,更易于阅读。它还会降低您的测试维护,因为您的依赖项较少。
我写了一篇关于单元测试的blog post,它扩展了这里提到的想法。它使用C#来解释概念,但它适用于所有语言。
答案 1 :(得分:0)
您的示例不会显示相互依赖的测试,而是针对使用相关类Router
和Route
的类RouteParameter
的单个测试。由于这些是数据持有者,我在Router
测试中使用它们没有问题。
在Wouter de Kort指出测试时,使用模拟对象非常有用,但请记住,存在权衡取舍。测试可能更难以阅读和维护,并且由于测试代码没有自己的测试,任何复杂性都是一种风险。