单一职责原则、测试驱动开发和功能设计
Single Responsibility Principle, Test Driven Development, and Functional Design
我是测试驱动开发的新手,我刚刚开始学习 SOLID 原则,所以我希望有人能帮助我。在 TDD 范式中开发单元测试和方法的上下文中,我在理解单一职责原则时遇到了一些概念上的困难。例如,假设我想开发一种从数据库中删除项目的方法——在我的代码看起来像下面这样之前......
我将从定义测试用例开始:
"Delete_Item_ReturnsTrue": function() {
//Setup
var ItemToDelete = "NameOfSomeItem";
//Action
var BooleanResult = Delete( ItemToDelete );
//Assert
if ( BooleanResult === true ) {
return true;
} else {
console.log("Test: Delete_Item_ReturnsTrue() - Failed.");
return false;
}
}
我会 运行 测试以确保它失败,然后我会开发方法...
function Delete( ItemToDelete ) {
var Database = ConnectToDatabase();
var Query = BuildQuery( ItemToDelete );
var QueryResult = Database.Query( Query );
if ( QueryResult.error !== true ) {
//if there's no error then...
return true;
} else {
return false;
}
}
如果我正确理解单一职责原则,我最初编写的方法负责删除项目并且 return 如果没有错误则为真。因此,如果我遵循 SOLID 范式,原始方法应该重构为类似于...
function Delete( WhatToDelete, WhereToDeleteItFrom ) {
WhereToDeleteItFrom.delete( WhatToDelete );
}
这样进行设计,将我的方法从布尔函数更改为 void 函数,所以现在我无法以测试旧方法的相同方式真正测试新方法。
我想我可以测试抛出的异常,但这不会使它成为集成测试而不是单元测试,因为方法中没有实际抛出的异常吗?
我是否设计并实现了一个额外的功能来检查我的单元测试中使用的数据库?
我只是因为它无效而不测试它吗?这在 TDD 中究竟是如何工作的?
我是否传入一个模拟存储库,然后在它的状态发生变化后 return 它?这不就是让我回到原点吗?
我是否传递对模拟存储库的引用,然后在方法完成后仅针对存储库进行测试?但这不会被视为副作用吗?
所以单一职责原则说:当我需要更改功能时,它们应该是一个确切的原因。
那么让我们看看你的功能:
function Delete( ItemToDelete ) {
var Database = ConnectToDatabase();
var Query = BuildQuery( ItemToDelete );
var QueryResult = Database.Query( Query );
if ( QueryResult.error !== true ) {
//if there's no error then...
return true;
} else {
return false;
}
}
- 数据库更改:ConnectToDatabese() 需要更改,但此函数不需要
- 数据库结构的变化:BuildQuery() 需要改变(可能)
- ...
乍一看,您的函数看起来不错。有些地方的命名有点混乱。函数应该以小写字母开头。例如 "connectToDatabase()"。 connectToDatabase returns 是一个对象有点令人惊讶。
名称 BuildQuery 似乎是错误的,因为 BuildQuery( myItem ) returns 一个正在删除某些内容的查询。
但我永远不会有这么长的复杂函数,只有一个测试用例。
您需要编写更多的测试用例。
对于第一个测试用例,您可以这样编写函数:
function Delete( ItemToDelete) {
return true
}
下一个测试用例可能是 "call the buildDeleteQuery function with the item id"。那时你需要考虑如何调用你的数据库。如果你有一个 dbObject,你可以模拟那个对象。
function Delete( ItemToDelete ) {
buildDeleteQuery( ItemToDelete.id )
return true
}
现在测试用例越来越多。请记住,还会有 buildDeleteQuery 的测试用例。但是对于外部函数,您可以模拟 buildDeleteQuery。
现在回答你的一些问题:
- 当你先写测试再写代码时,你就会有可测试的代码。
- 该代码看起来不一样,因为测试应该不会太复杂。当您想要简单的测试时,您将自动拥有更多更小的功能。
- 是的,您将为调用的每个函数编写一个测试。
- 也许你会模拟对象。如果您无法在没有包装函数的情况下测试您的数据库调用,您将创建一个函数,它允许您测试该功能。
- 请记住,您的测试是代码的文档。所以让它们尽可能简单。
但最重要的是:坚持练习!当您开始使用 TDD 时,需要一些时间。一个有趣的好资源是:Uncle Bobs Bowling Kata只需从网站下载幻灯片,看看 tdd 是如何一步步完成的。
我是测试驱动开发的新手,我刚刚开始学习 SOLID 原则,所以我希望有人能帮助我。在 TDD 范式中开发单元测试和方法的上下文中,我在理解单一职责原则时遇到了一些概念上的困难。例如,假设我想开发一种从数据库中删除项目的方法——在我的代码看起来像下面这样之前......
我将从定义测试用例开始:
"Delete_Item_ReturnsTrue": function() {
//Setup
var ItemToDelete = "NameOfSomeItem";
//Action
var BooleanResult = Delete( ItemToDelete );
//Assert
if ( BooleanResult === true ) {
return true;
} else {
console.log("Test: Delete_Item_ReturnsTrue() - Failed.");
return false;
}
}
我会 运行 测试以确保它失败,然后我会开发方法...
function Delete( ItemToDelete ) {
var Database = ConnectToDatabase();
var Query = BuildQuery( ItemToDelete );
var QueryResult = Database.Query( Query );
if ( QueryResult.error !== true ) {
//if there's no error then...
return true;
} else {
return false;
}
}
如果我正确理解单一职责原则,我最初编写的方法负责删除项目并且 return 如果没有错误则为真。因此,如果我遵循 SOLID 范式,原始方法应该重构为类似于...
function Delete( WhatToDelete, WhereToDeleteItFrom ) {
WhereToDeleteItFrom.delete( WhatToDelete );
}
这样进行设计,将我的方法从布尔函数更改为 void 函数,所以现在我无法以测试旧方法的相同方式真正测试新方法。
我想我可以测试抛出的异常,但这不会使它成为集成测试而不是单元测试,因为方法中没有实际抛出的异常吗?
我是否设计并实现了一个额外的功能来检查我的单元测试中使用的数据库?
我只是因为它无效而不测试它吗?这在 TDD 中究竟是如何工作的?
我是否传入一个模拟存储库,然后在它的状态发生变化后 return 它?这不就是让我回到原点吗?
我是否传递对模拟存储库的引用,然后在方法完成后仅针对存储库进行测试?但这不会被视为副作用吗?
所以单一职责原则说:当我需要更改功能时,它们应该是一个确切的原因。
那么让我们看看你的功能:
function Delete( ItemToDelete ) {
var Database = ConnectToDatabase();
var Query = BuildQuery( ItemToDelete );
var QueryResult = Database.Query( Query );
if ( QueryResult.error !== true ) {
//if there's no error then...
return true;
} else {
return false;
}
}
- 数据库更改:ConnectToDatabese() 需要更改,但此函数不需要
- 数据库结构的变化:BuildQuery() 需要改变(可能)
- ...
乍一看,您的函数看起来不错。有些地方的命名有点混乱。函数应该以小写字母开头。例如 "connectToDatabase()"。 connectToDatabase returns 是一个对象有点令人惊讶。
名称 BuildQuery 似乎是错误的,因为 BuildQuery( myItem ) returns 一个正在删除某些内容的查询。
但我永远不会有这么长的复杂函数,只有一个测试用例。
您需要编写更多的测试用例。 对于第一个测试用例,您可以这样编写函数:
function Delete( ItemToDelete) {
return true
}
下一个测试用例可能是 "call the buildDeleteQuery function with the item id"。那时你需要考虑如何调用你的数据库。如果你有一个 dbObject,你可以模拟那个对象。
function Delete( ItemToDelete ) {
buildDeleteQuery( ItemToDelete.id )
return true
}
现在测试用例越来越多。请记住,还会有 buildDeleteQuery 的测试用例。但是对于外部函数,您可以模拟 buildDeleteQuery。
现在回答你的一些问题:
- 当你先写测试再写代码时,你就会有可测试的代码。
- 该代码看起来不一样,因为测试应该不会太复杂。当您想要简单的测试时,您将自动拥有更多更小的功能。
- 是的,您将为调用的每个函数编写一个测试。
- 也许你会模拟对象。如果您无法在没有包装函数的情况下测试您的数据库调用,您将创建一个函数,它允许您测试该功能。
- 请记住,您的测试是代码的文档。所以让它们尽可能简单。
但最重要的是:坚持练习!当您开始使用 TDD 时,需要一些时间。一个有趣的好资源是:Uncle Bobs Bowling Kata只需从网站下载幻灯片,看看 tdd 是如何一步步完成的。