赞扬精心设计:基于属性的测试如何帮助我成为更好的开发人员

开发人员的测试工具箱就是其中之一,很少保持不变。 可以肯定的是,某些测试实践已被证明比其他测试更有价值,但是,我们仍在不断寻找更好,更快和更具表现力的方法来测试我们的代码。 基于属性的测试 Java社区中鲜为人知的 ,这是Haskell员工精心制作的另一种瑰宝,并在QuickCheck论文中进行了介绍

Scala社区(诞生了ScalaCheck库的Scala社区)和许多其他组织很快就意识到了这种测试技术的强大功能,但是Java生态系统在相当长的一段时间内一直缺乏采用基于属性的测试的兴趣。 幸运的是,自从jqwik出现以来,情况就在慢慢变化, 以求更好。

对于许多人来说,很难掌握什么是基于属性的测试以及如何利用它。 杰西卡·克尔Jessica Kerr)的精彩演讲《基于属性的测试,更好的代码》 ,以及有关基于属性的测试的简介,基于 属性的测试模式的系列文章,是吸引您的绝佳来源,但是在今天的帖子中,我们将尝试发现典型的Java开发人员使用jqwik进行的基于属性的测试的实践方面。

首先, 基于属性的测试名称实际上意味着什么? 每个Java开发人员的第一个想法都会是它旨在测试所有的getter和setter(您好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中,这意味着重写equalshashCode )。 这样,对于任何一对User类实例,以下不变量必须为true:

  • 如果两个User实例具有相同的用户名 ,则它们相等,并且必须具有相同的哈希码
  • 如果两个User实例具有不同的username ,则它们不相等(但哈希码不一定相同)

它非常适合基于属性的测试,并且jqwik尤其使这种测试的编写和维护变得微不足道。

 @Provide  Arbitrary&ltString> 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上找到完整的项目资源。

翻译自: https://www.javacodegeeks.com/2020/02/in-praise-of-the-thoughful-design-how-property-based-testing-helps-me-to-be-a-better-developer.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值