做个人吧,写点Testable代码,好吗!
Posted Danny_姜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了做个人吧,写点Testable代码,好吗!相关的知识,希望对你有一定的参考价值。
标题所说的Testable实际上指的是Unit Testable,也就是可单元测试,或者是易书写单元测试的代码。
注:不知道单元测试(Unit Test)是什么的,请自行Google
需求解析
那怎样才算是UnTestable的代码呢?又是什么原因导致很难对其书写单元测试代码呢?
可以通过一个实际需求来做详细分析,假设我们要实现一个智能灯光控制器,控制器的功能是根据当前所处时间模式,自动将灯光打开或者关闭。其中灯光的模式根据当前系统时间分为以下几种
1. 当时间在 0 ~ 6 点之间,则展示Night模式;
2. 当时间在 6 ~ 12 点之间,则展示Morning模式;
3. 当时间在 12 ~ 18 点之间,则展示Afternoon模式;
4. 其余时间则都默认展示 Evening 模式
上述需求并不复杂,仔细拆分下需求,基本包含以下2部分:
获取当前时间,根据时间返回所对应的主题
调用灯光设置接口(LightSwitcher)相应方法,切换灯光主题
实现代码如下:
public class SmartLightController {
public void switchLights() {
String timeOfDay = getTimeOfDay();
if ("Evening".equals(timeOfDay) || "Night".equals(timeOfDay)) {
LightSwitcher.getInstance().turnOn();
} else if ("Morning".equals(timeOfDay) || "Afternoon".equals(timeOfDay)) {
LightSwitcher.getInstance().turnOff();
}
}
public String getTimeOfDay() {
// 获取系统时间,并返回相应字符串 Night、Morning等
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 0 && hour < 6) return "Night";
if (hour >= 6 && hour < 12) return "Morning";
if (hour >= 12 && hour < 18) return "Afternoon";
return "Evening";
}
}
在上述实现中,getTimeOfDay()方法起着承上启下的作用。正如注释中描述一样,它的作用就是获取当前系统时间,并返回相应主题字符串的方法。
在SmartLightController中主要有2个方法,getTimeOfDay和switchLights。他们的作用依次是返回时间模式和执行开/关灯光操作。因此我们需要对这2个方法执行单元测试,以保证方法的准确性。
问题出在哪?
首先来看getTimeOfDay方法究竟有哪些问题呢?我们可以从2个角度分析这段代码存在的问题。
1. 从单元测试的角度看
如果我们想对这个方法进行单元测试是很困难的。原因在于Calendar.getInstance().get这个接口是跟设备实际情况绑定在一起的。比如我们想测试时间点在8点的时候,getTimeOfDay是否正确的返回"Morning"字符串,那就只能等到早上8点整,准时执行这个方法;或者我们可以在系统设置里手动的将时间设置为早上八点,然后执行此方法。最终单元测试会变成如下形式:
@Test
public void testGetTime_ReturnMorning() {
// 手动将设备时间修改为早上 6点~12点 之间
String time = getTimeOfDay();
assertEquals(time, "Morning");
}
可想而知,如果我们要对每一个时间点都进行单元测试,那会是一项极其耗时又耗力的工作。这严重违反了单元测试快速、独立的原则。
2. 从面向对象开发原则的角度
另一方面,从面向对象开发原则上来看,getTimeOfDay的实现也有待完善。首先它违反了SRP职责单一原则。从职责上来看,getTimeOfDay只需要处理当前时间,然后返回字符串即可。但是在其内部实现中,还承担了调用Calendar接口获取系统当前时间的任务。并且这也间接导致getTimeOfDay方法与设备的系统时间实现有严重的耦合。
代码重构
了解了问题的严重性之后,剩下的只需要将其完善即可。从之前的分析可以看出,似乎所有的问题都出在Calendar.getInstance().get()这行代码中,因为它是所有代码耦合的根源。因此解决办法也就是将其"解耦"。其实解耦方式很简单,我们只需要将其以参数的形式传入getTimeOfDay方法即可,如下所示:
public static String getTimeOfDay(int hour) {
if (hour >= 0 && hour < 6) return "Night";
if (hour >= 6 && hour < 12) return "Morning";
if (hour >= 12 && hour < 18) return "Afternoon";
return "Evening";
}
通过传入一个int类型的参数,使得getTimeOfDay方法的唯一职责就是判断hour参数,同事不再与系统时间的接口有任何耦合。并且接下来的单元测试代码也会变得非常简单,如下:
@Test
public void testGetTime_For8AM_ReturnMorning() {
int hour = 8;
String time = getTimeOfDay(hour);
assertEquals(time, "Morning");
}
似乎看起来,getTimeOfDay方法到目前为止已经变得Testable了,是不是就万事大吉了呢?很遗憾并没有,刚才我们将getTimeOfDay进行了重构,为其添加了hour参数,这虽然解决了它与Calendar API的耦合;但本质上是将锅甩给了它的调用者,也就是switchLights方法。因此经过修改后的完整代码如下:
很明显,问题并没有根治!如果要对switchLights方法书写单元测试方法,我们还是会遇到同样的问题。
接下来是不是需要像getTimeOfDay方法一样,继续将hour以参数的形式传入进来呢?虽然我们可以这样做,但是这种做法始终不能根治问题,只是将问题一级一级的往上抛而已。
那如何根治这个问题呢?众多解决方案中,有一种非常好的方式就是:依赖注入 Dependency Injection。
依赖注入
上文中也已经提到过,问题的根本在于对于Calendar的耦合,所以需要找一种方式对其进行解偶。在诸如java等面向对象语言中,依赖注入 Dependency Injection 就是一种很好的解偶方式。
所谓依赖注入,简单的理解就是如果组件Car依赖组件Engine,并不直接在组件Car中new出组件Engine,取而代之的是在外部创建出组件Engine,然后传递(注入)给组件Car,如下图:
上图来源于谷歌官方网站,具体内容详见: https://developer.android.com/training/dependency-injection
接下来,我们需要创建一个提供时间的接口IDateTimeProvider,如下:
public interface IDateTimeProvider {
int getDateTime();
}
然后通过SmartLightController的构造器,注入IDateTimeProvider接口实例对象。并且在getTimeOfDay方法中,通过接口IDateTimeProvider实例获取时间。如下:
public class SmartLightController {
private IDateTimeProvider dateTimeProvider;
public SmartHomeController(IDateTimeProvider dateTimeProvider) {
// 注入接口实例
this.dateTimeProvider = dateTimeProvider;
}
public void switchLights() {
// 由DateTimeProvider接口提供当前时间
int time = dateTimeProvider.getDateTime();
// ...
}
}
通过依赖注入的方式,我们将不同IDateTimeProvider的实现类传给SmartLightController类,这样就将SmartLightController和Calendar完全解耦。
单元测试
通过依赖注入实现解耦之后,再加上IDateTimeProvider接口加持,单元测试也变得极为简单。我们可以通过Fake Test Double的方式实现不同模式下的IDateTimeProvider,如下:
class FakeDateTimeProviderReturnMorning implements IDateTimeProvider {
@Override
public String getDateTime() {
return "Morning";
}
}
class FakeDateTimeProviderReturnAfternoon implements IDateTimeProvider {
@Override
public String getDateTime() {
return "Afternoon";
}
}
class FakeDateTimeProviderReturnEvening implements IDateTimeProvider {
@Override
public String getDateTime() {
return "Evening";
}
}
class FakeDateTimeProviderReturnNight implements IDateTimeProvider {
@Override
public String getDateTime() {
return "Night";
}
}
创建好上述Fake Test Double之后,就可以书写针对性的单元测试代码了,如下:
@Test
public void witchLight_ReturnMorning() {
IDateTimeProvider dateTimeProvider = new FakeDateTimeProviderReturnMorning();
SmartLightController sc = new SmartLightController(dateTimeProvider);
sc.switchLights();
assertEquals("Morning", sc.getLastLightMotion());
}
@Test
public void witchLight_ReturnAfternoon() {
IDateTimeProvider dateTimeProvider = new FakeDateTimeProviderReturnAfternoon();
SmartLightController sc = new SmartLightController(dateTimeProvider);
sc.switchLights();
assertEquals("Afternoon", sc.getLastLightMotion());
}
@Test
public void witchLight_ReturnEvening() {
IDateTimeProvider dateTimeProvider = new FakeDateTimeProviderReturnEvening();
SmartLightController sc = new SmartLightController(dateTimeProvider);
sc.switchLights();
assertEquals("Evening", sc.getLastLightMotion());
}
@Test
public void witchLight_ReturnNight() {
IDateTimeProvider dateTimeProvider = new FakeDateTimeProviderReturnNight();
SmartLightController sc = new SmartLightController(dateTimeProvider);
sc.switchLights();
assertEquals("Night", sc.getLastLightMotion());
}
注:为了测试方便,我在SmartLightController中添加了getLastLightMotion来获取当前保存的灯光模式。
总结
单元测试和代码质量是相辅相成的关系,好的代码很容易对其书写单元测试,通过单元测试也能提前预知代码中可能会出现的问题。
如果发现项目中的逻辑代码很难书写单侧,很有可能就是耦合性太高。这样的代码健壮性不高,后期扩展成本也很高。本文介绍了一种很常用的解耦方式:依赖注入。
实际在Android中,还有一种更为高级的依赖注入方式--Dagger。下篇文章将会在这篇文章的基础上,添加Dagger的使用,并介绍如何对Dagger依赖书写单侧代码。
另外,到目前为止SmartLightController实际上还没有完全达到100% Testable的程度,后续篇章也将继续对其做代码重构,使其达到完美状态。
对源码有需求,或者想一起探讨共同进步的,欢迎关注公众号发私信或者加微信。
如果你喜欢本文
长按二维码关注
以上是关于做个人吧,写点Testable代码,好吗!的主要内容,如果未能解决你的问题,请参考以下文章