软件工程发生在代码被非原作者阅读之时
Spock vs JUnit
单元测试框架,JUnit读者已了解,因此直接开门见山,基于JUnit和Spock做一个对比,明显Spock在工程化更有优势。
对比项 | Spock | JUnit |
结构可读性 | ✓ | |
问题定位效率 | ✓ | |
报告可读性 | ✓ | |
参数化测试 | ✓ | |
Mock能力 | ✓ |
结构可读性
Spock强制使用一套清晰的测试结构,而JUnit测试缺乏正式的语义。使用Spock的首要原因就是它可以使得你的测试更具可读性。如果你只是在周末业余时间创建一些小的项目,那这一点看起来就不那么重要了。然而对于大型的企业级项目来说,当你要在原有的代码基础上工作时,测试的可读性就显得犹为重要了。
如果你查看那些优秀的JUnit测试,你就会发现在它的结构里会有一种模式。这种结构就是非常著名的arrange-act-assert 模式,它将测试清晰的分为三个阶段,如下所示:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
}
其中arrange 阶段准备好了所需的类。
在上面的例子中,前两个语句就属于这个阶段(一个客户端和一个计费的服务对象被初始化)。act 是测试的触发。这个阶段应该会调用测试下面的主题的方法。在我们的用例中,主题就是计费服务。触发就是150美元的收费。assert 阶段是最后一步,它将触发的实际结果与我们期待的做对比。如果它们一致,则测试就成功了,否则测试就是失败的。
这个arrange-act-assert 结构只有开发人员考虑使用它时才会存在(也就是说并不是所有人都会用)。
上面的例子非常的简单,因此三个阶段就很容易看出来。我们可以对该测试进行改善,也就是如下所示的加上空行:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
}
然而在一些相当大的测试中,这些阶段并不总是那么明显。设想有一个大的JUnit测试,使用了复杂的业务逻辑。经验丰富的开发人员仍然会通过使用注释来标识出各个阶段,以便于提高其可读性:
@Test
public void veryComplexLoanApprovalScenario() {
//Prepare loan request and client details
[...lots of statements here...]
//Customer asks for a loan
[...lots of statements here...]
//Check that loan was approved
[...lots of assert statements here...]
}
注释的使用当然会使测试更加容易读懂了,但是这种技术远不是我们所理想的。
首先,大型的JUnit测试都是反模式的,都应该被重构使用私有的方法来满足业务需求。
其次,并非所有的开发人员都会注释,而这些注释也只能由程序员阅读到(比如说,它们永远不会出现在测试报告上)。
Spock测试使得arrange-act-assert结构显而易见,让我们使用Spock来重写下第一个测试:
public void "charging a credit card - happy path"() {
given: "a billing service and a customer with a valid credit card"
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
when: "client buys something with 150 dollars"
billing.charge(client,150);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 150
}
尽管该代码片段是采用Groovy书写的,我还是尽量保证跟Java相似些。首先你会注意到方法的名字是一段英语文字,明确的描述了该测试所要做的事情(之后会有更多说明)。其次要注意的是一堆given-when-then的标签将代码分成了三部分。很明显的,这三部分直接就是与JUnit的arrange-act-assert中的部分相对应的。在given块里的语句即是arrange 阶段,而在when 块里的则对应于act阶段,assert阶段在Spock里则由then代码块所实现。
相对于使用普通的注释(就像Junit例子中所示那样)来说,Spock的标签有两个比较大的优点:
1. Spock标签是完整的英语文本,并且也同时会展示在测试报告中(之后还会有更多说明)
2. Spock标签同时还会有语义值。
第二点最好是可以通过一个反面的例子来说明。
在JUnit测试中,arrange-act-assert 模式是隐含的。让我们假设一个顽皮的Java开发人员修改JUnit测试成如下这般摸样:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
Client client2 = new Client();
billing.charge(client2,100);
assertEquals("Revenue should be recorded",250,billing.getCurrentRevenue());
}
在这个简单的测试中,还是可以很容易就理解到底发生了什么。但是在更大的测试中,增加额外的断言和设置语句会使得测试难于阅读理解。Spock可以使得测试结构非常清晰。除了基本的given-when-then,Spock还有其他几种代码块可以用来保证测试的结构比较清晰。
这个顽皮的开发人员,不会以类似的方式修改Spock的测试。相反的,一个更好的Spock测试如下所示:
public void "charging a credit card - two transactions"() {
given: "a billing service ready to accept payments"
CreditCardBilling billing = new CreditCardBilling();
and: "two customers with valid credit cards"
Client client1 = new Client();
Client client2 = new Client();
when: "first client buys something with 150 dollars"
billing.charge(client1,150);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 150
when: "second client buys something with 100 dollars"
billing.charge(client2,100);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 250
}
这里我们使用了and 代码块来合并两个arrange 阶段。而when和then代码块使得该测试阅读起来很自然流畅。仅仅通过阅读代码块的名字(given-and-when-then-when-then),我们就可以很清楚的知道这个测试是做什么的。
问题定位效率
Spock在测试失败时会提供更有用的信息,这是Spock应用到我自己的项目里的最牛逼的一个特性。
永远不要相信一个你没看到其失败的测试。 – Colin Vipurs
单元测试的首要目的是发现回归中的错误。经常会发生一个测试在之前的构建中通过而在当前的构建中却失败了的情况。
你想让你的测试失败,这看起来似乎违反直觉。会失败的测试就是能工作的测试。
JUnit的测试出现失败时总是需要调试(才能找到问题原因)。不幸的是,JUnit测试失败了通常就需要进行调试,因为在失败的情况下所给出的信息是最基本的(不够详细)。
这里有一个为演示需要故意实现的例子:
@Test
public void similarBooks() {
Book book = new Book("The Murder on the Links");
List<String> similar = book.findSimilarTitles();
assertEquals("Murder on the Orient Express",similar.get(0));
}
假设你的项目的最新的构建失败了。你检查了本地的代码,运行了所有的测试,然后得到了如下结果:
只是看这个失败提示并没什么帮助。你知道是哪个测试失败了,但是却很难弄明白如何去修复这个失败。
在这个例子中,你需要在调试器里运行该测试才可以弄清楚为什么你得到了一个跟你预期不一致的值。
Spock 为您提供足够的上下文信息, 假定你使用Spock重写了同样的测试:
public void "find books with similar titles"() {
given: "A book that contains the word murder"
Book book = new Book("The Murder on the Links")
when: "we search similar books"
List<String> similar = book.findSimilarTitles()
then: "similar books should have murder in the title"
similar.get(0) == "Murder on the Orient Express"
}
这个测试当然会失败,但是这次你会得到如下提示:
跟JUnit不同,Spock知道失败的上下文信息。在这个例子中,Spock为你展示了相似图书的整个列表的内容。仅仅通过查看这个失败的测试你就能梦看到你要搜索的图书名确实是在列表中,但是是在第二个位置。这个测试希望该值位于第一位,因此测试就失败了。
现在你就知道你的代码失败是由于一个索引的变化(或者是一段代码将相似图书的顺序做了调整)。有了这些知识,你就可以更快的查明问题原因。
报告可读性
Spock测试可以被非技术人员所读懂。
单元测试的命名在企业级应用里很重要。开发人员通常认为由测试产生的报告中有技术性术语会有什么不妥。
计算机科学领域有两大难题:缓存失效和命名问题。– Phil Karlton
JUnit里的方法名称受到Java规范限制。一个典型的错误就是在单元测试里使用不知道说些什么的标题。下面是一个极其反面的例子:
这个命名“scheme”的问题是它对于手头没有代码的人来说一点用都没有。如果测试名为“scenario2”的方法因为某种原因失败了,除了开发人员没有其他人能够明白它会造成如何影响。经验丰富的Java开发者会试图将他们的单元测试使用其真实的意图来命名。这样明显会更好些,但是仍然不够完美,因为它们由于Java语言的限制,只能采用驼峰式命名(或者下划线):
Spock 支持简单的英语句子。
Spock测试的优美之处,在于其可以使用完整的英语句子描述来作为其方法名(这在前面部分的例子中已经有所体现了)。
Maven(和其他任何JUnit相关的工具)可以对它们进行格式化,且无需额外的配置:
然而Spock可以将这份报告做进一步改善。Spock拥有其自己的报告模块,可以显示包含在Spock的块代码(比given,when,then)的所有文本。结果如下:
这对于Spock测试的可读性来说是个很大的改进。
非技术人员 也可以读懂该报告,并且在无需知道Java是如何工作的前提下就能做出合适的抉择。
测试者 可以通过阅读Spock测试,来跟他们自己的测试用例进行对比。
业务分析人员 可以通过阅读Spock测试,来验证它们是否是按照系统规格要求来实现的。
项目管理者 可以通过阅读Spock报告,从而清楚系统目前的状态。
他们可以及时的发现一个失败的测试会产生什么样(或高或低)的影响。
参数化测试
Spock对于参数化测试有一个自定义的DSL。
在JUnit里,一个非常常见的反模式,是有相当一部分测试是99.9%的地方都是相同的,而只有一些变量是不一样的。
请看如下例子:
@Test
public void acceptJpg() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "scenery.jpg";
assertTrue(validator.isValidImageExtension(pictureFile));
}
@Test
public void acceptJpeg() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "house.jpeg";
assertTrue(validator.isValidImageExtension(pictureFile));
}
@Test
public void doNotAcceptTiff() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "sky.tiff";
assertFalse(validator.isValidImageExtension(pictureFile));
}
这些JUnit测试很显然不符合DRY原则(Don't repeat yourself,不要重复自己),因为它们有太多的相同代码了。
实际上它们都是相同的测试逻辑(传递一个图片给ImageValidator类),而且唯一变化的东西就是文件名和期待的结果。
以下图文可以更好的解释这些测试的相似性:
这种类型的测试被叫做参数化的测试,因为它们拥有相同的测试逻辑,而且所有的场景均依赖于传递给该测试逻辑的不同的参数。
JUnit对于参数化的支持非常有限而且很受限制
使用JUnit进行参数化测试是可以完成的,但是由此导致的语法却是相当的丑陋。我建议你去阅读下JUnit的官方文档。 我会等着你回来的。:)
如果你从未见过JUnit参数化测试的步骤的话,不用担心。这里做个清晰的说明:
- 它需要一个自定义的运行期runner(@RunWith(Parameterized.class));
- 测试类必须添加用于标识输入的字段;
- 测试类必须添加用于标识输出的字段;
- 需要一个特殊的构造器来注入所有的输入和输出;
- 测试数据会被装载到一个二维对象数组中(之后会被转换为一个列表);
- 测试数据和测试描述是分开的;
- 你无法很容易的地在同一个类中使用两个测试。
基本上来说,这个由JUnit提供的“解决方案”显得非常麻烦而且吃力不讨好,这也是为什么大量的Java开发人员并不知道JUnit参数化测试的存在。
我知道有很多外部的类库可以增强实现JUnit参数化测试的方式,但是它们的存在进一步加强我的论点,
即Spock不像JUnit那样,是自带电池的(译者注:这里是指支持参数化测试的方式)。
Spock以一种直观的方式编写参数化测试。
Spock可以提供数据表格来使得单元测试更容易理解。
由于其使用了Groovy DSL,因此使得你可以讲数据和其描述以表格的方式放在一起:
public void "Valid images are PNG and JPEG files"() {
given: "an image extension checker"
ImageNameValidator validator = new ImageNameValidator()
expect: "that only valid filenames are accepted"
validator.isValidImageExtension(pictureFile) == validPicture
where: "sample image names are"
pictureFile || validPicture
"scenery.jpg" || true
"house.jpeg" || true
"car.png" || true
"sky.tiff" || false
"dance_bunny.gif" || false
}
这里你可以看到三个JUnit的测试被合并为一个单独的Spock测试,这里有几个明显的优点:
- 不存在代码重复。测试逻辑只需要编写一次;
- 所有的输入和输出都集中在一个地方(即where代码块);
- 参数的名称在表格的头部清晰可见。 Spock数据表格的灵活性随着测试的增长会突显其强大之处。 增加一个新的测试场景仅需要增加一行即可。 在上面的例子中,我增加了两个场景,即png和gif的图像格式,只花了很少的代码。
增加一个新的输入或者输出变量也是相当的容易,因为你只需要给表格增加一列即可。
有趣的是,如果你运行单独的这一个Spock测试,而且使用了Spock 的 Unroll 注解的话,实际上会运行多个测试(每一行都会运行一个测试)。你甚至可以让每个测试运行单独使用自定义字符串进行命名,这样每个测试运行都可以描述它做了什么:
@Unroll("Running image #pictureFile with result #validPicture")
public void "Valid images are PNG and JPEG files"() {
given: "an image extension checker"
ImageNameValidator validator = new ImageNameValidator()
expect: "that only valid filenames are accepted"
validator.isValidImageExtension(pictureFile) == validPicture
where: "sample image names are"
pictureFile || validPicture
"scenery.jpg" || true
"house.jpeg" || true
"car.png" || true
"sky.tiff" || false
"dance_bunny.gif" || false
}
这里是运行结果:
这对于拥有大量数目的测试来说尤其有用。如果其中一个测试失败了,你就可以在测试报告中看出哪个测试失败了(而不是将整个测试都标记为失败)。
Spock数据表格是参数化测试最基本的形式。Spock同样支持数据管道和自定义迭代器,可以满足对输入输出参数处理的更强大的需求。
在以后的文章里我们将会探索Spock所提供的关于参数化测试的所有功能 - 因为它们值得我们对其单独进行分析。
mocking能力
Spock拥有内置的mocking和stubbing功能。
当谈到mocking时,JUnit真没有可比性,因为JUnit甚至还不支持mocking。
当你需要在JUnit中用到mocking的时候,你就会需要引入独立的框架来支持。
Java已经有几个mocking框架了,但是最近占主导地位的是Mockito。
在另一方面,Spock则致力于满足你所有的测试需求,因此在基本包里就内置了强大的mock和stubs的支持。
注意: 如果你还不知道什么是mocking,或者从来没使用过Mockito,那么你首先就应该去阅读下这篇关于Mockito的入门文章:
Stubbing and Mocking with Mockito 2 and JUnit。
使用Spock实现基本的Stubbing
Spock可以使用其自己更为简单的语法实现典型的Mockito套路语句:
when(something).thenReturn(somethingElse) 。
Spock不会引入两个新的方法(when 和 then),而是私用>>操作符,意思是“返回那个东西”。
举个例子吧,让我们假定你在为一个批准贷款的银行编写单元测试。以下是核心逻辑
public class LoanApprover {
public boolean approveLoan(Customer customer, long amount){
if(amount < 1000){
return true;
}
if(amount < 50000 && customer.hasGoodCreditScore()){
return true;
}
return false;
}
}
我们来同时看看使用Mockito 和Spock所实现的相同单元测试分别是怎样的:
//JUnit/Mockito Test method
@Test
public void goodCredit(){
Customer sampleCustomer = mock(Customer.class);
when(sampleCustomer.hasGoodCreditScore()).thenReturn(true);
LoanApprover loanApprover = new LoanApprover();
assertTrue(loanApprover.approveLoan(sampleCustomer, 10000));
}
//Spock Test method
public void "customer with good credit and loan of 10000 should be approved"() {
given: "a customer with good credit"
Customer sampleCustomer = Stub(Customer.class)
sampleCustomer.hasGoodCreditScore() >> true
expect: "an approval of the loan"
LoanApprover loanApprover = new LoanApprover()
loanApprover.approveLoan(sampleCustomer, 10000) == true
}
在这个简单的例子中,你可以看到,Spock和Mockito工作的方式很相近。Spock里的插入符语法将Mockito里的when/thenReturn语法合二为一。
值得注意的是,与Mockito不同,Spock清楚知道虚假对象的本质(比如,是stub,还是mock)。在这个特定的例子中,我只是查询了Customer来获取一个返回结果,因此在Spock里我们创建了一个Stub,而不是一个Mock。这里区别是微小的,但是在接下来的部分就会显得更加明显了。
Mockito里对于stubbing的各种你所喜欢的特性,在Spock里同样也是支持的。我不会在这里继续深入详细阐述,但是像多次返回值、匹配特定的参数,或者是创建自定义的响应,这些在Spock里都是很容易实现的(而且通常来说语法更简单些)。
使用Spock实现基本的Mocking
我们假定LoanApprover 类更加聪明了些。
如果贷款被审批或者被拒后,它不会返回一个布尔类型的结果,而是发了一封邮件。
以下是代码:
public class LoanApproverWithEmail {
private final EmailService emailService;
public LoanApproverWithEmail(final EmailService emailService){
this.emailService = emailService;
}
public void approveLoan(Customer customer, long amount){
if(loanApproved(customer, amount)){
emailService.sendConfirmation(customer.getEmailAddress());
}
else{
emailService.sendRejection(customer.getEmailAddress());
}
}
private boolean loanApproved(Customer customer, long amount){
if(amount < 1000){
return true;
}
if(amount < 50000 && customer.hasGoodCreditScore()){
return true;
}
return false;
}
}
这一次,我们利用构造注入来使用一个外部的电子邮件服务,为了达到我们的目的,它有两个方法,分别叫sendConfirmation()和sendRejection()。
由于我们的测试方法 -approveLoan - 是带有void返回值的(既无返回值),我们这里就不能使用stub来编写单元测试了。我们需要mock 这个EmailService,并且检查它在单元测试完成之后做了些什么。
编写一个Mockito 测试很简单(假定已经很熟悉Mockito)。我们需要使用Mockito的 verify 指令,而不是JUnit的断言。
@Test
public void lowAmountIsAlwaysAccepted(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
//Loans that low will be accepted regardless of credit score
loanApprover.approveLoan(sampleCustomer, 600);
verify(emailService).sendConfirmation(sampleCustomer.getEmailAddress());
verify(emailService,times(0)).
sendRejection(sampleCustomer.getEmailAddress());
}
@Test
public void bigAmountsAreAlwaysRejected(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
//Loans that high will be rejected regardless of credit score
loanApprover.approveLoan(sampleCustomer, 75000);
verify(emailService,times(0)).
sendConfirmation(sampleCustomer.getEmailAddress());
verify(emailService).sendRejection(sampleCustomer.getEmailAddress());
}
这两个单元测试mock了EmailService 类。因此一旦贷款被请求,我们就会检查发送到客户那里的邮件的类型。如果是发送的是一封确认信,那么我们就知道贷款被审批通过了。如果发送的是拒绝信,我们就知道代码没有被审批通过。
为了让单元测试更加严谨些,我们还需要验证确实只给客户发送了一种类型的邮件。对于客户来说,如果对于同一次贷款申请同时收到了拒绝信和确认信,那将会是很尴尬的事情。
在这种特定情况下,对于电子邮件服务的mocking就至关重要了,因此我们也要同时要避免每次测试运行时都发送一封真实邮件的情况发生。
现在我们看看使用Spock如何实现同样的测试:
public void "very low loan amounts are always rejected"() {
given: "a customer with any credit"
Customer sampleCustomer = new Customer()
and: "an email service that is mocked"
EmailService emailService = Mock(EmailService.class)
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
when: "customer requests a loan lower than 1000 USD"
loanApprover.approveLoan(sampleCustomer, 600);
then: "a confirmation email is sent to the customer"
1 * emailService.sendConfirmation(sampleCustomer.getEmailAddress())
0 * emailService.sendRejection(sampleCustomer.getEmailAddress())
}
public void "very high loan amounts are always rejected"() {
given: "a customer with any credit"
Customer sampleCustomer = new Customer()
and: "an email service that is mocked"
EmailService emailService = Mock(EmailService.class)
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
when: "customer requests a loan higher than 50000 USD"
loanApprover.approveLoan(sampleCustomer, 75000);
then: "a rejection email is sent to the customer"
0 * emailService.sendConfirmation(sampleCustomer.getEmailAddress())
1 * emailService.sendRejection(sampleCustomer.getEmailAddress())
}
与Mockito类似,没有使用JUnit断言来实现。
相反的,使用了一种特殊的Spock语法来进行方法验证。其格式如下:
N * mockedObject.method(arguments)
这一行意思是:“在测试结束之后,mockedObject对象的method 方法使用参数arguments时应该只被执行过N次”。如果事实确实如此发生,那么测试就会通过。否则Spock就会将测试标记为失败。
这种语法比Mockito更加干净些,因为你无须指定verify 和times指令。这样的验证代码更接近于真正的Java代码。
同时要注意,被模拟的对象这次是通过Mock()来创建的,而不像前面的例子里通过Stub()创建。
Spock 使读者能够分清楚哪些类是用来检测模拟结果的(stubs),而哪些又是用于验证的(mocks),然而Mockito却没有做这种区分。
从技术上讲,测试在这两种情况下工作是一样的,但是考虑到可读性的话,Spock的方式明显要更好一些,特别是对于那些有很多虚假对象要创建的大型的单元测试来说。
Spock 匹配器(以及为什么它们比Mockito要好些)
假定我们的电子邮件服务在邮件发送时会记录下其时间。发送邮件的两个方法都增加了这个参数。
public interface EmailService {
void sendConfirmation(String emailAddress, LocalDateTime when);
void sendRejection(String emailAddress, LocalDateTime when);
}
我们同时假定该日期事先是不知道的。也许它是当前日期,也许是下一个工作日,也许是周末,我们并不关心。但是我们得模拟它。Mockito提供了几个匹配器(matchers)用来忽略参数的实际值。不幸的是,你可能已经知道了,Mockito并不支持使用真实参数的混合匹配器。我们先用Mockito来实现一下:
(如果你是Mockito的老手,你就会知道这个测试根本就无法运行。)
这里Mockito清楚地告诉我们我们需要对所有的参数都要使用匹配器。
为了克服这种限制,我们修改单元测试,并使用anyString匹配器忽略掉了email方法的第一个参数。
@Test
public void lowAmountIsAlwaysAccepted(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithDate loanApprover = new LoanApproverWithDate(emailService);
//Loans that high will be rejected regardless of credit score
loanApprover.approveLoan(sampleCustomer, 50000);
verify(emailService,times(0)).
sendConfirmation(anyString(), any());
verify(emailService).
sendRejection(anyString(), any());
}
现在测试可以正确运行了。然而这并不是我们想要的那样严格。因为电子邮件地址现在也被忽略掉了,我们也就无法确定电子邮件地址是正确的并且真实的反映了用户的电子邮件地址。在这个认为编造出来的例子中,看起来确实不像什么大问题,但是在真实的单元测试里,这个Mockito的限制却有可能将bug带入到生产环境中。
跟Mockito一样,Spock支持忽略方法参数并使用下划线字符 _ 来标识它们。然而与Mockito 不同的是, 它却是支持匹配器与真实参数混合出现的情形。因此我们原来的忽略日期并检查邮件的测试在Spock里就是可以直接支持的。
这个测试正确运行,因为Spock确实支持匹配器与真实参数同时出现。
<span class="lake-selected" data-card-type="inline" data-lake-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fintranetproxy.alipay.com%2Fskylark%2Flark%2F0%2F2019%2Fpng%2F7923%2F1575356856999-96b47da6-5ffc-4727-b52d-5dcec2a4abd9.png%22%2C%22originWidth%22%3A769%2C%22originHeight%22%3A163%2C%22name%22%3A%22image.png%22%2C%22size%22%3A19306%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22ocrLocations%22%3A%5B%7B%22x%22%3A-0.8010417%2C%22y%22%3A24.031252%2C%22width%22%3A151.39688170000002%2C%22height%22%3A13.617708000000004%2C%22text%22%3A%22Finishedafter0.964seconds%22%7D%2C%7B%22x%22%3A142.58542%2C%22y%22%3A51.26667%2C%22width%22%3A55.2718800