赞扬别人团建评论
开发人员的测试工具箱就是其中之一,很少保持不变。 可以肯定的是,一些测试实践已被证明比其他测试更有价值,但是,我们仍在不断寻找更好,更快和更具表现力的方法来测试我们的代码。 基于属性的测试 是 Java社区中鲜为人知的 ,这是Haskell员工精心制作的另一种瑰宝,并在QuickCheck论文中进行了介绍 。
Scala社区(诞生了ScalaCheck库的Scala社区)和许多其他组织很快意识到了这种测试技术的强大功能,但是Java生态系统在相当长的一段时间内一直缺乏采用基于属性的测试的兴趣。 幸运的是,自从jqwik出现以来,情况就在慢慢变化, 以求更好。
对于许多人来说,很难掌握什么是基于属性的测试以及如何利用它。 杰西卡·克尔 ( Jessica Kerr)的精彩演讲《基于属性的测试,更好的代码》 ,以及有关基于属性的测试的简介,基于 属性的测试模式系列的文章,是吸引您的绝佳来源,但是在今天的帖子中,我们将尝试发现典型的Java开发人员使用jqwik进行的基于属性的测试的实践方面。
首先, 基于属性的测试名称实际上意味着什么? 每个Java开发人员首先想到的是它旨在测试所有的getter和setters(您好100%覆盖率)吗? 并非如此,尽管对于某些数据结构而言可能很有用。 相反,我们应该确定组件,数据结构甚至单个功能的高级特性 ,并通过提出假设有效地对其进行检验。
我们的第一个示例属于“那里又回来”类别:将序列化和反序列化为JSON表示形式。 被测试的类是User POJO ,虽然很简单,但请注意它具有OffsetDateTime类型的一个时间属性。
public class User {
private String username;
@JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS[SSS]]XXX" , shape = Shape.STRING)
private OffsetDateTime created;
// ... }
令人惊讶的是,由于所有人都在尝试使用自己的表示形式,因此最近几天使用日期/时间属性进行操作会引起问题。 如您所见,我们的合同使用的是ISO-8601互换格式,带有可选的毫秒部分。 我们要确保的是,可以将任何有效的User实例序列化为JSON ,然后反序列化为Java对象,而不会失去任何日期/时间精度。 作为练习,让我们尝试首先用伪代码来表达它:
For any user
Serialize user instance to JSON
Deserialize user instance back from JSON
Two user instances must be identical
看起来很简单,但令人惊讶的部分出在这里:让我们看一下如何使用jqwik库将此伪代码投影到真实的测试用例中。 它尽可能地接近我们的伪代码。
@Property void @ForAll serdes( @ForAll ( "users" ) User user) throws JsonProcessingException {
final String json = serdes.serialize(user);
assertThat(serdes.deserialize(json))
.satisfies(other -> {
assertThat(user.getUsername()).isEqualTo(other.getUsername());
assertThat(user.getCreated().isEqual(other.getCreated())).isTrue();
});
Statistics.collect(user.getCreated().getOffset()); }
测试用例读起来很简单,通常是自然的,但是显然, jqwik的@Property和@ForAll批注后面隐藏着一些背景。 让我们从@ForAll开始,并清除所有这些User实例的来源。 您可能猜到了,这些实例必须生成,最好以随机方式生成。
对于大多数内置数据类型, jqwik具有一组丰富的数据提供程序( Arbitraries ),但是由于我们要处理特定于应用程序的类,因此我们必须提供自己的生成策略。 它应该能够发出具有广泛的用户名以及不同时区和偏移量集合的日期/时刻的User类实例。 首先让我们先略过提供者的实现,然后再详细讨论。
@Provide Arbitrary<User> users() {
final Arbitrary<String> usernames = Arbitraries.strings().alpha().ofMaxLength( 64 );
final Arbitrary<OffsetDateTime> dates = Arbitraries
.of(List.copyOf(ZoneId.getAvailableZoneIds()))
.flatMap(zone -> Arbitraries
.longs()
.between(1266258398000L, 1897410427000L) // ~ +/- 10 years
.unique()
.map(epochMilli -> Instant.ofEpochMilli(epochMilli))
.map(instant -> OffsetDateTime.from(instant.atZone(ZoneId.of(zone)))));
return Combinators
.combine(usernames, dates)
.as((username, created) -> new User(username).created(created)); }
用户名的来源很简单:只是随机字符串。 日期的来源基本上可以是2010年到2030年之间的任何日期/时间,而时区部分(因此是偏移量)是从所有可用的基于区域的区域标识符中随机选择的。 例如,下面是jqwik提出的一些示例。
{ "username" : "zrAazzaDZ" , "created" : "2020-05-06T01:36:07.496496+03:00" } { "username" : "AZztZaZZWAaNaqagPLzZiz" , "created" : "2023-03-20T00:48:22.737737+08:00" } { "username" : "aazGZZzaoAAEAGZUIzaaDEm" , "created" : "2019-03-12T08:22:12.658658+04:00" } { "username" : "Ezw" , "created" : "2011-10-28T08:07:33.542542Z" } { "username" : "AFaAzaOLAZOjsZqlaZZixZaZzyZzxrda" , "created" : "2022-07-09T14:04:20.849849+02:00" } { "username" : "aaYeZzkhAzAazJ" , "created" : "2016-07-22T22:20:25.162162+06:00" } { "username" : "BzkoNGzBcaWcrDaaazzCZAaaPd" , "created" : "2020-08-12T22:23:56.902902+08:45" } { "username" : "MazNzaTZZAEhXoz" , "created" : "2027-09-26T17:12:34.872872+11:00" } { "username" : "zqZzZYamO" , "created" : "2023-01-10T03:16:41.879879-03:00" } { "username" : "GaaUazzldqGJZsqksRZuaNAqzANLAAlj" , "created" : "2015-03-19T04:16:24.098098Z" } ...
默认情况下, jqwik将针对1000套不同的参数值(随机用户实例)运行测试。 非常有用的“ 统计”容器允许收集您好奇的任何分布见解。 以防万一,为什么不按区域偏移量收集分布呢?
...
- 04 : 00 ( 94 ) : 9.40 %
- 03 : 00 ( 76 ) : 7.60 %
+ 02 : 00 ( 75 ) : 7.50 %
- 05 : 00 ( 74 ) : 7.40 %
+ 01 : 00 ( 72 ) : 7.20 %
+ 03 : 00 ( 69 ) : 6.90 %
Z ( 62 ) : 6.20 %
- 06 : 00 ( 54 ) : 5.40 %
+ 11 : 00 ( 42 ) : 4.20 %
- 07 : 00 ( 39 ) : 3.90 %
+ 08 : 00 ( 37 ) : 3.70 %
+ 07 : 00 ( 34 ) : 3.40 %
+ 10 : 00 ( 34 ) : 3.40 %
+ 06 : 00 ( 26 ) : 2.60 %
+ 12 : 00 ( 23 ) : 2.30 %
+ 05 : 00 ( 23 ) : 2.30 %
- 08 : 00 ( 20 ) : 2.00 %
...
让我们考虑另一个例子。 想象一下,我们决定基于用户名属性重新实现User类的相等性(在Java中,这意味着重写equals和hashCode )。 这样,对于任何一对User类实例,以下不变量必须为true:
- 如果两个User实例具有相同的用户名 ,则它们相等,并且必须具有相同的哈希码
- 如果两个User实例具有不同的username ,则它们不相等(但哈希码不一定相同)
它非常适合基于属性的测试,并且jqwik尤其使这种测试的编写和维护变得微不足道。
@Provide Arbitrary<String> usernames() {
return Arbitraries.strings().alpha().ofMaxLength( 64 ); } @Property void equals( @ForAll ( "usernames" ) String username, @ForAll ( "usernames" ) String other) {
Assume.that(!username.equals(other));
assertThat( new User(username))
.isEqualTo( new User(username))
.isNotEqualTo( new User(other))
.extracting(User::hashCode)
.isEqualTo( new User(username).hashCode()); }
由于我们引入了用户名的两个来源,因此通过假设表达的假设允许对生成的参数施加额外的约束,这可能会发生,因为它们在同一运行中都发出相同的用户名,因此测试将失败。
您到目前为止可能遇到的问题是:重点是什么? 当然可以在不进行基于属性的测试和使用jqwik的情况下测试序列化/反序列化或equals / hashCode ,那么为什么还要麻烦呢? 足够公平,但是这个问题的答案基本上深深在于我们如何进行软件系统的设计。
总体而言, 基于属性的测试在很大程度上受函数式编程的影响,这并不是Java的第一件事(至少现在还不是),这要轻描淡写地说。 随机生成测试数据本身并不是一个新颖的主意,但是至少在我看来, 基于属性的测试鼓励您去做的是,以更抽象的角度思考,而不是专注于单个操作(等于,比较,加法)。 ,排序,序列化...),但是它们要遵循什么样的属性,特征,定律和/或不变式。 当然,这感觉就像是一种外来技术,如果您愿意的话,可以改变范式,鼓励您花更多的时间设计正确的东西。 这并不意味着从现在开始您的所有测试都必须基于属性,但是我相信它当然应该在我们的测试工具箱的前排位置。
请在Github上找到完整的项目资源。
赞扬别人团建评论