考虑这样的函数:
function savePeople(dataStore, people) {
people.forEach(person => dataStore.savePerson(person));
}
可以这样使用:
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
让我们假设它Store
具有自己的单元测试,或由供应商提供。无论如何,我们都相信Store
。让我们进一步假设错误处理(例如,数据库断开错误)不是的责任savePeople
。确实,让我们假设商店本身是一个神奇的数据库,它不可能以任何方式出错。 鉴于这些假设,问题是:
应该savePeople()
进行单元测试,还是将这些测试等同于测试内置forEach
语言结构?
当然,我们可以传递一个模拟dataStore
并断言dataStore.savePerson()
每个人都会被调用一次。您当然可以说这样的测试可提供针对实现更改的安全性的论据:例如,如果我们决定forEach
用传统的for
循环或某种其他迭代方法代替。因此,测试并非完全无关紧要。但是它似乎非常接近...
这是另一个可能更富有成效的例子。考虑一个仅协调其他对象或功能的功能。例如:
function bakeCookies(dough, pan, oven) {
panWithRawCookies = pan.add(dough);
oven.addPan(panWithRawCookies);
oven.bakeCookies();
oven.removePan();
}
假设您认为这样的功能应该如何进行单元测试?这是我很难想象任何种类的单元测试的不只是嘲笑dough
,pan
和oven
,然后断言方法被调用它们。但是这样的测试无非是复制了该函数的确切实现。
无法以有意义的黑盒方式测试功能是否表明功能本身存在设计缺陷?如果是这样,如何改进?
为了更清楚地说明激发bakeCookies
示例的问题,我将添加一个更现实的场景,该场景是我尝试向其添加代码并重构遗留代码时遇到的一种情况。
当用户创建新帐户时,需要做很多事情:1)需要在数据库中创建新用户记录2)需要发送欢迎电子邮件3)需要记录用户的IP地址以防欺诈目的。
因此,我们想创建一个将所有“新用户”步骤联系在一起的方法:
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
请注意,如果这些方法中的任何一个引发错误,我们都希望该错误冒泡到调用代码中,以便它可以按需处理错误。如果API代码正在调用它,则可能会将错误转换为适当的http响应代码。如果通过Web界面调用它,则可能会将错误转换为适当的消息以显示给用户,依此类推。关键是该函数不知道如何处理可能引发的错误。
我的困惑的实质是,要对这样的功能进行单元测试,似乎有必要在测试本身中重复确切的实现(通过指定以某种顺序调用模拟方法),这似乎是错误的。