做个人吧,写点Testable代码,好吗!
发布于 2021-10-10 23:58
标题所说的Testable实际上指的是Unit Testable,也就是可单元测试,或者是易书写单元测试的代码。
注:不知道单元测试(Unit Test)是什么的,请自行Google
可以通过一个实际需求来做详细分析,假设我们要实现一个智能灯光控制器,控制器的功能是根据当前所处时间模式,自动将灯光打开或者关闭。其中灯光的模式根据当前系统时间分为以下几种
1. 当时间在 0 ~ 6 点之间,则展示Night模式;
2. 当时间在 6 ~ 12 点之间,则展示Morning模式;
3. 当时间在 12 ~ 18 点之间,则展示Afternoon模式;
4. 其余时间则都默认展示 Evening 模式
获取当前时间,根据时间返回所对应的主题
调用灯光设置接口(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";
}
}
在SmartLightController中主要有2个方法,getTimeOfDay和switchLights。他们的作用依次是返回时间模式和执行开/关灯光操作。因此我们需要对这2个方法执行单元测试,以保证方法的准确性。
首先来看getTimeOfDay方法究竟有哪些问题呢?我们可以从2个角度分析这段代码存在的问题。
1. 从单元测试的角度看
@Test
public void testGetTime_ReturnMorning() {
// 手动将设备时间修改为早上 6点~12点 之间
String time = getTimeOfDay();
assertEquals(time, "Morning");
}
2. 从面向对象开发原则的角度
另一方面,从面向对象开发原则上来看,getTimeOfDay的实现也有待完善。首先它违反了SRP职责单一原则。从职责上来看,getTimeOfDay只需要处理当前时间,然后返回字符串即可。但是在其内部实现中,还承担了调用Calendar接口获取系统当前时间的任务。并且这也间接导致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方法一样,继续将hour以参数的形式传入进来呢?虽然我们可以这样做,但是这种做法始终不能根治问题,只是将问题一级一级的往上抛而已。
那如何根治这个问题呢?众多解决方案中,有一种非常好的方式就是:依赖注入 Dependency Injection。
上文中也已经提到过,问题的根本在于对于Calendar的耦合,所以需要找一种方式对其进行解偶。在诸如java等面向对象语言中,依赖注入 Dependency Injection 就是一种很好的解偶方式。
所谓依赖注入,简单的理解就是如果组件Car依赖组件Engine,并不直接在组件Car中new出组件Engine,取而代之的是在外部创建出组件Engine,然后传递(注入)给组件Car,如下图:
上图来源于谷歌官方网站,具体内容详见: https://developer.android.com/training/dependency-injection
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完全解耦。
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的程度,后续篇章也将继续对其做代码重构,使其达到完美状态。
本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。
相关素材