遗留代码存在于许多公司的代码库中。通常在开发过程中,有一些反模式会造成障碍。

我最近添加了我的第一个分析事件,作为我刚刚完成的一些功能工作的一部分。我偶然发现了一些意想不到的问题,它们是错误模式的后果。在本文中,我将向您展示如何使用抽象和改进的可测试性来克服它们。

收尾

我们有一个特定于特性或流的类叫做会话度量. 它包含发送与我们的用户会话相关的分析事件的所有必要部分。它的大多数方法将数据作为输入,然后创建并发送特定于我们的分析库的事件。它还定义了我们随事件发送的公共值。

object SessionMetrics:BaseMetrics(){const val conva\u STATUS=“conversation STATUS”fun fireSelectedMsg(/*lots of params*/){//创建事件firevent(event)}}

这个会话度量上面的扩展自父类基本指标. 父级非常小,是直接调用分析库的包装器。它还包含一个方便的方法,用于创建特定于我们的分析库的事件。

下面是一个什么样的例子基本指标看起来像。

打开类BaseMetrics{fun fireEvent(event:event){val实例=AnalyticsTool.getInstance实例() } }

问题

作为我们工作的一部分,我们需要从ViewModel添加一个新的分析事件。天真地,我们遵从了上面的模式。代码按预期运行,但很快我们就遇到了一个失败的单元测试。我们的静态构造直接调用分析库,结果ViewModel测试失败。

线路val实例=AnalyticsTool.getInstance实例()从上面的例子中可以看出,这是问题的根源。我们的AnalyticsTool从未打算在JVM中运行,因此破坏了我们的单元测试。我们能做什么?我们不能就这样离开。

我们的目标是什么?

至少,我们的变化不应该打破一个考验。在最好的情况下,除了不破坏现有的测试之外,我们还希望通过测试来验证我们的更改。让我们志存高远,看看能想出什么办法!

从一个接口开始

我们先创建一个接口,定义要发送到分析客户端的值。

接口SessionMetrics{fun fireSelectedMsg(inboxSizeProperty:Int?,targetUserId:String?,matchedUser:用户?,缩写:Int?)}

我们正在调用我们的接口会话度量它将有我们的事件方法fireSelectedMsg()它接收我们的分析活动所需的所有数据。

创建实现

类AnalyticsLibSessionMetrics:SessionMetrics,BaseMetrics(){重写fireSelectedMsg(inboxSizeProperty:Int?,targetUserId:String?,matchedUser:用户?,缩写:Int?){//从传入的数据fireEvent(analyticsLibEvent)创建分析库特定的事件//

然后我们像上面那样创建具体的实现并命名它分析分离度.名称应反映此特定实现正在使用的分析库。在这种情况下是这样的分析SLIB. 它实现了我们的会话度量除了实用程序之外,还有上面的接口基本指标.例如,如果您使用的是FirebaseFirebaseSessionMetrics公司

现在我们可以将新创建的类传递给ViewModel,如下所示:

MessageThreadViewModel(AnalyticsLibSessionMetrics())

用一个假的

让我们创建一个表示我们声明的接口的伪接口来重新启动并运行测试。

类FakeSessionMetrics:SessionMetrics{重写fun fireSelectedMsg(inboxSizeProperty:Int?,targetUserId:String?,matchedUser:用户?,缩写:Int?){//什么也不做}

通过声明我们的假代码,我们可以从测试中创建ViewModel,如下所示:

val fakeSessionMetrics=fakeSessionMetrics()消息线程视图模型(fakeSessionMetrics)

现在我们的测试又开始工作了,因为我们并没有真正调用我们的分析库。感觉我们可以做得更好!让我们验证是否确实调用了该事件。

验证是否使用了假的

让我们将一个实例变量添加到假货.

类FakeSessionMetrics:SessionMetrics{var selectedMsgEventFired=false override fun firesectedMsg(//大量参数){selectedMsgEventFired=true}}

实例变量已激发SelectedMsgEvent在调用假事件方法时设置为true。然后我们可以通过测试验证fireSelectedMsg()方法被调用。

断言(fakeSessionMetrics.selectedMsgEventFired事件).isTrue()

有些事还是不对

我们的fireSelectedMsg()方法获取的值不是我们传递给分析客户端的值。我们对数据进行一些处理,以便得出一些值,然后发送给我们的分析客户端。代码库中的当前模式将把这个处理逻辑放在ViewModel中。我现在的设计在我们新创建的会话度量它似乎不属于任何一个国家。

为什么它不属于这两个国家?

我们的ViewModel实际上不应该负责处理发送到分析层的数据。它增加了ViewModel的责任,使我们更难验证此处理中的逻辑。

如果我们把这个处理逻辑放在助手的具体实现中会话度量,我们无法测试它。我们将有一个失败的测试,因为我们直接调用我们的分析库的原始问题。处理逻辑是否与我们的具体实现有关?

数据作为类型

让我们看看到底我们将发送给我们的分析客户端。

val hasRead:Boolean,val targetUserId:String?,val ageInDays:Int,val inboxSize:Int

接下来,我们可以创建一个数据类来表示这些值。

数据类SelectedMessageEvent(val hasRead:Boolean,val targetUserId:String?,val ageInDays:Int,val inboxSize:Int)

我们的会话度量接口现在可以更改。

接口SessionMetrics{fun fireSelectedMsg(selectedMessageEvent:selectedMessageEvent)}

这简化了接口,并将分析库与处理逻辑分离。我们现在已经提前了解了我们的分析库对这次活动的期望。

处理逻辑

我们可以将处理逻辑与数据类关联起来,因为它们在逻辑上是一致的。我们可以附加一个静态方法发件人()我们的数据类,比如:

数据类SelectedMessageEvent(val hasRead:Boolean,val targetUserId:String?,val ageInDays:Int,val inboxSize:Int){伴随对象{来自(inboxSizeProperty:Int)的乐趣?,targetUserId:String?,matchedUser:用户?,缩写:Int?):SelectedMessageEvent{//处理逻辑}

我们对ViewModel的调用如下所示:

val事件=SelectedMessageEvent.from从(inSize、userId、matchedUser、nWays)sessionMetrics.fireSelectedMsg(事件)

这一切意味着什么

我们的ViewModel现在减少了责任。它创建了一个事件,尽管它不知道如何创建。它将事件发送到它不知道具体实现的接口。这一切都导致了测试的改进。我们现在可以在测试时断言我们的分析层被调用了。

分析层现在正在使用我们的自定义数据类和不需要处理的派生值。我们现在明确了这个层需要什么。如果将来我们想替换我们的分析库或添加另一个实现,它只需要使用我们的新数据类。

用于创建事件的处理逻辑现在已经被隔离,可以进行测试。我们可以提供大量的输入,并验证事件数据类是否按预期生成。

我们的测试现在可以工作了,我们可以验证ViewModel中的正确行为,并且可以验证事件数据类是否正确创建。

我希望你喜欢这篇文章,请随时联系我推特.

有兴趣为爱神丘比特工作吗?188bet金宝搏官网我们正在招聘!