Data layer testing (2)

今天会继续写 EtaResponseMapperTest。我们示范的 test case 是正常输出班次的情景。首先是准备 response:

val response = mockHttpResponse(
    statusCode = HttpStatusCode.OK,
    etaResponse = EtaResponse(
        status = EtaResponse.STATUS_NORMAL,
        message = "successful",
        isDelay = EtaResponse.IS_DELAY_FALSE,
        data = mapOf(
            "TKL-TKO" to EtaResponse.Data(
                up = listOf(
                    EtaResponse.Eta(
                        plat = "1",
                        time = "2020-01-11 14:28:00",
                        dest = "POA",
                        seq = "1",
                    ),
                    EtaResponse.Eta(
                        plat = "1",
                        time = "2020-01-11 14:36:00",
                        dest = "LHP",
                        seq = "2",
                    ),
                ),
                down = listOf(
                    EtaResponse.Eta(
                        plat = "2",
                        time = "2020-01-11",
                        dest = "XXX",
                        seq = "",
                    ),
                ),
            ),
        ),
    ),
)

在正常的情况下 server 会提供上行及下行的班次。在我们的实作中,上下行其实是用相同方法处理,那我们就在上行的 array 放一些正常的内容然後在下行放有问题的内容,看看我们的 mapper 能否正常处理。

接着就是 assertion 的部分。我们先检查输出的结果是不是 EtaResult.Success,然後用 and 开了一个 lambda。

expectThat(mapper.map(response)).isA<EtaResult.Success>().and {
    // 入面的 this 就变了 AssertionBuilder<EtaResult.Success>
    // 这样就可以继续深入检查 EtaResult.Success 的 property
}

之後我们就会按照 EtaResult.Success 的每一个 property 检查。它只有一个 property schedule ,我们再可以开另一层的 lambda 检查 List<EtaResult.Success.Eta> 入面的内容。

针对 collection 的部分,Strikt 提供了几个 method 方便我们做 assertion:

  • isNotEmpty() 检查 collection 是不是空
  • hasSize(3) 检查 collection 是不是有 3 个元素
  • get(2) 拿 index 第 2 的元素
  • first() 就是上面 get 的特别版,只取第一个元素
  • containsExactly(1, 2, 3) collection 入面必须要有这些元素,而且要按同样次序出现
  • containsExactlyInAnyOrder(1, 2, 3) 和上面差不多,只是没规定先後次序

更多的用法可以参考 Strikt 网站。之後我们可以再驳 and 来进入那一个元素作检查。一般我们都是会先用 get 取得那个 property 的 AssertionBuilder 然後再接驳那些 isEqualToisNull 之类的 assertion。留意这个 get 不是 get(0) 那个,parameter 用数字是用来取得 collection 的某一个元素;但如果 parameter 是用 property/method reference 或者 lambda 就是用来取得那个 property。

以下是 property/method reference 和 lambda 写法例子:

val subject: EtaResult.Success.Eta = EtaResult.Success.Eta(...)
expectThat(subject) {
    // property/method reference
    get(EtaResult.Success.Eta::platform).isEqualTo("1")
    // lambda
    get { platform }.isEqualTo("1")
}

两者效果都是一样,但 Strikt 网站指出用 lambda 写法效能上比用 property/method reference 差。所以如果可以的话都尽量用 property/method reference。

以下是整个 assertion 的部分:

expectThat(mapper.map(response)).isA<EtaResult.Success>().and {
    get(EtaResult.Success::schedule).isNotEmpty().and {
        get(0).and {
            get(EtaResult.Success.Eta::direction).isEqualTo(EtaResult.Success.Eta.Direction.UP)
            get(EtaResult.Success.Eta::platform).isEqualTo("1")
            get(EtaResult.Success.Eta::time).isEqualTo(
                ZonedDateTime.of(
                    2020, 1, 11, 14, 28, 0, 0,
                    DEFAULT_TIMEZONE
                ).toInstant()
            )
            get(EtaResult.Success.Eta::destination).isEqualTo(Station.POA)
            get(EtaResult.Success.Eta::sequence).isEqualTo(1)
        }
        get(1).and {
            get(EtaResult.Success.Eta::direction).isEqualTo(EtaResult.Success.Eta.Direction.UP)
            get(EtaResult.Success.Eta::platform).isEqualTo("1")
            get(EtaResult.Success.Eta::time).isEqualTo(
                ZonedDateTime.of(
                    2020, 1, 11, 14, 36, 0, 0,
                    DEFAULT_TIMEZONE
                ).toInstant()
            )
            get(EtaResult.Success.Eta::destination).isEqualTo(Station.LHP)
            get(EtaResult.Success.Eta::sequence).isEqualTo(2)
        }
        get(2).and {
            get(EtaResult.Success.Eta::direction).isEqualTo(EtaResult.Success.Eta.Direction.DOWN)
            get(EtaResult.Success.Eta::platform).isEqualTo("2")
            get(EtaResult.Success.Eta::time).isEqualTo(Instant.EPOCH)
            get(EtaResult.Success.Eta::destination).isEqualTo(Station.UNKNOWN)
            get(EtaResult.Success.Eta::sequence).isEqualTo(0)
        }
    }
}

留意因为 Instant 有 override equalshashCode,还有是 immutable,所以我们可以直接建构一个新的 Instant 跟输出做比较,如果那个 class 没有正确地 override equalshashCode 的话,还是要逐个 property call getter 检查比较安全。

你或许会问为甚麽我们不直接建构一个塞好我们期望的内容的 EtaResult.Success object 然後直接 isEqualTo 做比对。这是因为我想善用 Strikt 这类 assertion library 的优势。Strikt 这类 assertion library 如果 assertion 出问题的话它会输出详细的错误讯息让你看。

@Test
fun incident() = runBlockingTest {
    val response = mockHttpResponse(
        statusCode = HttpStatusCode.OK,
        etaResponse = EtaResponse(
            status = EtaResponse.STATUS_ERROR_OR_ALERT,
            message = "Special train service arrangements are now in place on this line.",
            url = "https://www.mtr.com.hk",
        ),
    )
    expectThat(mapper.map(response)).isA<EtaResult.Incident>().and {
        get(EtaResult.Incident::message).isEqualTo("Special train service arrangements are now in place on this line.")
        get(EtaResult.Incident::url).isEqualTo("https://www.mtr.com.hk/alert/alert_title_wap.html")
    }
}
▼ Expect that Incident(message=Special train service arrangements are now in place on this line., url=https://www.mtr.com.hk):
  ✓ is an instance of net.swiftzer.etademo.domain.EtaResult$Incident
  ▼ value of property message:
    ✓ is equal to "Special train service arrangements are now in place on this line."
  ▼ value of property url:
    ✗ is equal to "https://www.mtr.com.hk/alert/alert_title_wap.html"
            found "https://www.mtr.com.hk"

如果直接用 isEqualTo 的话,那 assertion 会写成:

expectThat(mapper.map(response)).isA<EtaResult.Incident>().isEqualTo(
    EtaResult.Incident(
        message = "Special train service arrangements are now in place on this line.",
        url = "https://www.mtr.com.hk/alert/alert_title_wap.html",
    )
)

之後输出的错误讯息都是把那个 class 的 toString 放给你自己看,但 property 一多你就很难检查:

▼ Expect that Incident(message=Special train service arrangements are now in place on this line., url=https://www.mtr.com.hk):
  ✓ is an instance of net.swiftzer.etademo.domain.EtaResult$Incident
  ✗ is equal to Incident(message=Special train service arrangements are now in place on this line., url=https://www.mtr.com.hk/alert/alert_title_wap.html)
          found Incident(message=Special train service arrangements are now in place on this line., url=https://www.mtr.com.hk)

EtaResponseMapperTest 的其他 test case 其实写法都大同小异,所以我就不逐一介绍。有兴趣的话可以直接去 GitHub 看 code

下一篇会写 EtaRepositoryImplTest


<<:  Ruby on Rails View Helper

>>:  DAY14: HTPP服务器:Respone对象

[Tableau Public] day 20:制作第三张仪表板

在新增仪表板窗格前,我们先新增一张工作表,名称为「app总数」,我们选择栏位「F1」,度量选择「计数...

26/一起成为国际研讨会讲师!!!(投稿篇)

CFP是Call for Papers/Call for Proposals的缩写,中文可以称作是研...

强制访问控制(MAC)- 安全许可(Security clearance)

-安全内核 一张图片胜过千言万语。访问控制矩阵可以被视为授权数据(权利和许可)的逻辑“存储库”,由...

JS 逻辑运算子及函式预设值 DAY56

逻辑运算子 MDN : https://developer.mozilla.org/zh-TW/do...

Day 0x5 UVa10062 Tell me the frequencies!

Virtual Judge ZeroJudge 题意 对每一列输入,输出各字元的 ASCII &a...