快照测试_什么是快照测试,并且在PHP中可行?

快照测试

This article was peer reviewed by Matt Trask, Paul M. Jones, and Yazid Hanifi. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

本文由Matt TraskPaul M. JonesYazid Hanifi进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!



A vector image of a polaroid glued to a transparent background

Ah-ha moments are beautiful and rare in programming. Every so often, we’re fortunate enough to discover some trick or facet of a system that forever changes how we think of it.

啊哈时刻在编程中是美好而罕见的。 每隔一段时间,我们很幸运地发现系统的某些花招或方面,这些花样或方面会永远改变我们的想法。

For me, that’s what snapshot testing is.

对我来说,这就是快照测试。

You probably write a lot of PHP code, but today I want to talk about something I learned in JavaScript. We’ll learn about what snapshot testing is and then see how it can help us write better PHP applications.

您可能编写了很多PHP代码,但是今天我想谈谈我在JavaScript中学到的东西。 我们将了解什么是快照测试,然后了解它如何帮助我们编写更好PHP应用程序。

建筑界面 (Building Interfaces)

Let’s talk about React. Not the kick-ass async PHP project, but the kick-ass JavaScript project. It’s an interface-generation tool in which we define what our interface markup should look like as discrete parts:

让我们谈谈React。 不是kick-ass异步PHP项目,而是kick-ass JavaScript项目。 这是一个界面生成工具,其中我们定义了界面标记作为离散部分的外观:

function Tweet(props) {
  return (
    <div className="tweet">
      <img src={props.user.avatar} />
      <div className="text">
        <div className="handle">{props.user.handle}</div>
        <div className="content">{props.content}</div>
      </div>
    </div>
  )
}

function Tweets(props) {
  return (
    <div className="tweets">
      {props.tweets.map((tweet, i) => {
        return (
          <Tweet {...tweet} key={i} />
        )
      })}
    </div>
  )
}

This doesn’t look like vanilla Javascript, but rather an unholy mix of HTML and Javascript. It’s possible to create React components using regular Javascript syntax:

这看起来不像原始的Javascript,而是HTML和Javascript的不完美结合。 可以使用常规Javascript语法创建React组件:

function Tweet(props) {
  return React.createElement(
    "div",
    { className: "tweet" },
    React.createElement("img", { src: props.user.avatar }),
    React.createElement(
      "div",
      { className: "text" },
      React.createElement(
        "div",
        { className: "handle" },
        props.user.handle
      ),
      React.createElement(
        "div",
        { className: "content" },
        props.content
      )
    )
  );
}

To make this code, I pasted the Tweet function (above) into the Babel REPL. That’s what all React code is reduced to (minus the occasional optimization) before being executed by a browser.

为了编写此代码,我将Tweet函数(上面)粘贴到Babel REPL中 。 这就是所有React代码在被浏览器执行之前都会减少(减去偶尔的优化)的过程。

Before I talk about why this is cool, I want to address a couple of issues…

在谈论为什么这很酷之前,我想解决一些问题……

“为什么要混合HTML和Javascript ?!” (“Why Are You Mixing HTML and Javascript?!”)

We’ve spent a lot of time teaching and learning that markup shouldn’t be mixed with logic. It’s usually couched in the phrase “Separation of Concerns”. Thing is, splitting HTML and the Javascript which makes and manipulates that HTML is largely without value.

我们花了很多时间来教导和学习标记不应与逻辑混合在一起。 通常用“关注分离”一词表示。 问题是,将HTML和创建和处理HTML的Javascript拆分为没有价值的东西。

Splitting that markup and Javascript isn’t so much separation of concerns as it is separation of technologies. Pete Hunt talks about this in more depth in this video.

分离标记和Javascript并不是关注点的分离,而是技术的分离。 皮特·亨特(Pete Hunt) 在本视频中对此进行了更深入的讨论。

“此语法非常奇怪” (“This Syntax Is Very Strange”)

That may be, but it is entirely possible to reproduce in PHP and works out the box in Hack:

可能是这样,但是完全有可能在PHP中复制并找出Hack中的框:

class :custom:Tweet extends :x:element {
  attribute User user;
  attribute string content;

  protected function render() {
    return (
      <div class="tweet">
        <img src={$this->:user->avatar} />
        <div class="text">
          <div class="handle">{$this->:user->handle}</div>
          <div class="content">{$this->:content}</div>
        </div>
      </div>
    );
  }
}

I don’t want to in detail about this wild syntax except to say that this syntax is already possible. Unfortunately, it appears the official XHP module only supports HHVM and old versions of PHP…

除了要说这种语法已经可行之外,我不想详细介绍这种语法。 不幸的是, 官方XHP模块似乎仅支持HHVM和旧版本PHP…

测试界面 (Testing Interfaces)

There are many testing approaches – some more effective than others. An effective way to test interface code is by faking (or making) a web request and inspecting the output for the presence and content of specific elements.

测试方法很多,有些比其他方法更有效。 测试接口代码的有效方法是伪造(或发出)Web请求并检查输出中特定元素的存在和内容。

Perhaps you’ve heard of things like Selenium and Behat? I don’t want to dwell too much on them. Let’s just say that Selenium is a tool we can use to pretend to be a browser, and Behat is a business-friendly language for scripting such pretense.

也许您听说过Selenium和Behat之类的东西? 我不想过多地关注他们。 可以说Selenium是一种我们可以假装成浏览器的工具,而Behat是一种商业友好的语言,可以编写这种假装。

Unfortunately, a lot of browser-based testing can be brittle. It’s tied to the exact structure of markup, and not necessarily related to the logic that generates the markup.

不幸的是,许多基于浏览器的测试可能很脆弱。 它与标记的确切结构相关联,并不一定与生成标记的逻辑相关。

Snapshot testing is a different approach to doing the same thing. React encourages thinking about the whole interface in terms of the smallest pieces it can be broken down into. Instead of building the whole shopping cart, it encourages breaking things up into discrete parts; like:

快照测试是执行相同操作的另一种方法。 React鼓励从可以分解为最小部分的角度考虑整个界面。 与其建立整个购物车,不如将其分解为离散的部分。 喜欢:

  • each product

    每个产品
  • the list of products

    产品清单
  • the shipping details

    运送细节
  • the progress indicator

    进度指示器

In building each of these pieces, we define what the markup and styles should be, given any initial information. We define this by creating a render method:

在构建所有这些片段时,我们会在给出任何初始信息的情况下定义标记和样式。 我们通过创建一个render方法来定义它:

class Tweets extends React.Component {
  render() {
    return (
      <div className="tweets">
        {props.tweets.map((tweet, i) => {
          return (
            <Tweet {...tweet} key={i} />
          )
        })}
      </div>
    )
  }
}

…or by defining a plain function which will return a string or React.Component. The previous examples demonstrated the functional approach.

…或通过定义将返回字符串或React.Component的普通函数。 前面的示例演示了功能方法。

This is an interesting way of thinking about an interface. We write render as though it’ll only be called once, but React is constantly reconciling changes to the initial information, and the component’s own internal data.

这是考虑接口的一种有趣方式。 我们编写render就像它只会被调用一次一样,但是React一直在协调对初始信息和组件自身内部数据的更改。

And it’s this way of thinking that leads to the simplest way to test React components: Snapshot Testing. Think about it for a minute…

正是这种思维方式导致了测试React组件的最简单方法:快照测试。 考虑一下……

We build React components to render themselves, given any initial information. We can work through all possible inputs in our head. We can even define strict validation for what initial information (or properties) we allow into our components.

给定任何初始信息,我们将构建React组件来呈现自己。 我们可以解决所有可能的想法。 我们甚至可以为允许在组件中使用的初始信息(或属性)定义严格的验证。

So, if we can work through those scenarios while designing our component: then we can work through them in a test. Let’s create a new component:

因此,如果我们在设计组件时可以解决这些情况:那么我们可以在测试中解决它们。 让我们创建一个新组件:

const Badge = function(props) {
  const styles = {
    "borderColor": props.borderColor
  }

  if (props.type === "text") {
    return (
      <div style={styles}>{props.text}</div>
    )
  }

  return (
    <img style={styles} src={props.src} alt={props.text} />
  )
}

This is our Badge component. It can be of two types: text and image. It can also have a border color. We can validate and create defaults for these properties:

这是我们的Badge组件。 它可以有两种类型: textimage 。 它也可以具有边框颜色。 我们可以验证并为这些属性创建默认值:

const requiredIf = function(field, value, error) {

  // custom validators expect this signature
  return function(props, propName, componentName) {

    // if props.type === "image" and props.src is not set
    if (props[field] === value && !props[propName]) {

      return new Error(error)
    }
  }
}

Badge.propTypes = {
  "borderColor": React.PropTypes.string,
  "type": React.PropTypes.oneOf(["text", "image"]),
  "src": requiredIf("type", "image", "src required for image")
}

Badge.defaultProps = {
  "borderColor": "#000",
  "type": "text"
}

So, what are the ways in which this component can be used? There are a few variations:

那么,可以使用什么方式使用此组件? 有几种变体:

  • Without specifying a borderColor or type, a text badge is rendered and it has a black border.

    如果不指定borderColortype ,则呈现文本徽章,并且该徽章具有黑色边框。

  • Changing type to image requires that src is also set, and renders an image badge.

    type更改为image需要同时设置src并呈现图像标志。

  • Changing borderColor to anything changes the border color of text and image badges alike.

    borderColor更改为任何值都会更改文本和图像标记的边框颜色。

It’s beginning to sound like a test. What if we called the rendered component with a well-defined initial set of data, a snapshot? We could describe these scenarios with some JavaScript:

听起来像是测试。 如果我们用定义良好的初始数据集(即快照)来调用渲染组件该怎么办? 我们可以使用一些JavaScript描述这些场景:

import React from "react"
import Tweets from "./tweets"

import renderer from "react-test-renderer"

test("tweets are rendered correctly", function() {
  const defaultBadge = renderer.create(
    <Tweets>...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()

  const imageBadge = renderer.create(
    <Tweets type="image" src="path/to/image">...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()

  const borderedBadge = renderer.create(
    <Tweets borderColor="red">...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()
})

We need to get Jest set up, before we can run this code.

在运行此代码之前,我们需要设置Jest

With each modification to the initial properties, a new set of markup may be rendered. We should check that each variation matches a snapshot we know to be accurate.

通过对初始属性进行每次修改,可以呈现一组新的标记。 我们应该检查每个变化是否匹配我们知道准确的快照。

With this, we can see that snapshot testing is a means by which we can predict the output of a given function (or the internal state of a given object), and compare future output to it. With a well-defined blueprint of the data we expect our code to give, we can easily tell when the output is unexpected or incorrect.

这样,我们可以看到快照测试是一种我们可以预测给定函数的输出(或给定对象的内部状态)并与之比较未来输出的方法。 有了我们希望我们的代码给出的明确定义的数据蓝图,我们可以轻松分辨出何时输出意外或错误。

This has an older name: Characterization Testing. We write characterization (or snapshot) tests by establishing well-formed, expected output. Subsequent tests compare the output of our code against the baseline, using the same inputs.

它有一个较旧的名称: Characterization Testing 。 我们通过建立格式正确的预期输出来编写表征(或快照)测试。 随后的测试使用相同的输入将我们的​​代码输出与基准进行比较。

Now, let’s think of scenarios where it could be useful to us…

现在,让我们考虑一下可能对我们有用的方案……

PHP中的快照测试 (Snapshot Testing in PHP)

We’re going to look at a few use-cases for snapshot testing. Before we do, let me introduce you to a PHPUnit snapshot testing plugin: https://github.com/spatie/phpunit-snapshot-assertions

我们将看一些快照测试的用例。 在开始之前,让我向您介绍一个PHPUnit快照测试插件:https://github.com/spatie/phpunit-snapshot-assertions

You can install it via:

您可以通过以下方式安装它:

composer require --dev spatie/phpunit-snapshot-assertions

It provides a number of helpers in the form of:

它以以下形式提供了许多帮助者:

use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testExamples()
  {
    $this->assertMatchesSnapshot(...);
    $this->assertMatchesXmlSnapshot(...);
    $this->assertMatchesJsonSnapshot(...);
  }
}

These methods create the same kinds of snapshot files as Jest does. In general, we only need to use the first one, but the second and third generate nicer diffs for their respective content types.

这些方法创建的快照文件与Jest相同。 通常,我们只需要使用第一个,而第二个和第三个会为它们各自的内容类型生成更好的差异。

范本 (Templates)

The first, and sort of obvious, use for this is in testing templates. The smaller our templates, the easier it should be to write snapshot tests for them. This is particularly useful when using template engines which accept initial properties:

首先,也是显而易见的用途是在测试模板中。 我们的模板越小,应该为它们编写快照测试就越容易。 当使用接受初始属性的模板引擎时,这特别有用:

<div class="tweet">
  <img src={{ $user->avatar }} />
  <div class="text">
    <div class="handle">{{ $user->handle }}</div>
    <div class="content">{{ $content }}</div>
  </div>
</div>
Route::get("/first-tweet", function () {
  return view(
    "tweet", Tweet::first()->toArray()
  );
});

It’s clear what the template should produce, given repeatable initial information. We could even mock the initial data, and assert the output:

给定可重复的初始信息,很明显模板应该产生什么。 我们甚至可以模拟初始数据,并声明输出:

namespace Tests\Unit;

use Spatie\Snapshots\MatchesSnapshots;
use Tests\TestCase;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testTweetsRenderCorrectly()
  {
    $user = new User();
    $user->avatar = "path/to/image";
    $user->handle = "assertchris";

    $tweet = new Tweet();
    $tweet->user = $user;
    $tweet->content = "Beep boop!";

    $rendered = view(
      "tweet", $tweet
    );

    $this->assertMatchesSnapshot($rendered);
  }
}

I’m showing examples from a Laravel application (buy our introductory course here!). Laravel uses PHPUnit under the hood, so the examples would work outside of Laravel too, with enough modification. Laravel is a useful starting-point because it ships with a template engine, routing, and an ORM.

我正在展示Laravel应用程序中的示例( 在这里购买我们的入门课程!)。 Laravel在后台使用了PHPUnit,因此,经过足够的修改,这些示例也可以在Laravel之外运行。 Laravel是一个有用的起点,因为它附带了模板引擎 ,路由和ORM。

活动采购 (Event Sourcing)

Event Sourcing architectures are particularly well-suited to snapshot testing. There’s a lot to them, so feel free to read up on them!

事件源架构特别适合快照测试。 他们有很多东西,所以请随时阅读它们

The basic idea behind Event Sourcing is that the database is write-only. Nothing is ever deleted, but every meaningful action leads to an event record. In contrast with most CRUD applications which freely create and delete the latest state of records, Event Source applications add records which represent all the actions.

事件源背后的基本思想是数据库是只写的。 什么都不会被删除,但是每一个有意义的动作都会导致一个事件记录。 与大多数CRUD应用程序自由创建和删除记录的最新状态相反,事件源应用程序添加代表所有操作的记录。

Snapshot testing works with or without Event Sourcing, but testing an event stream is tedious without it. Imagine being able to assert the latest state of a record, but also effortlessly assert every step that got the record to that state.

快照测试在有或没有事件源的情况下都可以使用,但是如果没有事件源,则测试事件流很繁琐。 想象一下,既可以声明记录的最新状态,又可以毫不费力地声明使记录达到该状态的每个步骤。

// continuing from sitepoint.com/event-sourcing-in-a-pinch...

$events = [
  new ProductInvented(...),
  new ProductPriced(...),
  new OutletOpened(...),
  new OutletStocked(...),
];

$this->assertMatchesSnapshot($events);

$projected = project($connection, $events);

$this->assertMatchesSnapshot($projected);

Even before these assertions, it’s useful to simulate browser activity (or whatever input medium this application enables) to assert whether interface interactions generate a snapshot that matches the snapshot of generated events.

甚至在这些声明之前,模拟浏览器活动(或此应用程序启用的任何输入介质)以声明接口交互是否生成与生成的事件的快照匹配的快照也是有用的。

排队的任务 (Queued Tasks)

Asynchronous systems are particularly troublesome to test. At worst we ignore these parts of our application. At best, we struggle through the process of mocking the queue provider for every deferred interaction with the system. What if we could test all queued tasks at once, for our entire test suite?

异步系统特别难以测试。 最糟糕的是,我们会忽略应用程序的这些部分。 充其量,我们要为每个与系统进行的延迟交互而模拟队列提供程序的过程而苦苦挣扎。 如果我们可以为整个测试套件一次测试所有排队的任务怎么办?

php artisan queue:table
php artisan migrate

This creates a database table, for the database queue driver.

这将为数据库队列驱动程序创建一个数据库表。

return [
  "default" => env("QUEUE_DRIVER", "database"),
  // ...remaining configuration
];

This is from config/queue.php.

这来自config/queue.php

namespace Tests\Unit;

use Spatie\Snapshots\MatchesSnapshots;
use Tests\TestCase;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testQueueSomething()
  {
    // ...do something that leads to dispatch(new Job);
  }

  public function testQueueSomethingAgain()
  {
    // ...do something that leads to dispatch(new AnotherJob);
  }

  public function testQueue()
  {
    $table = config("queue.connections.database.table");
    $jobs = app("db")->select("select * from {$table}");

    $this->assertMatchesSnapshot($jobs);
  }
}

Once the jobs are deleted, they’ll no longer be returned by the select statement. You’ll have to make sure no queue listener/daemon is running for the same database table while the tests are running.

作业删除后,select语句将不再返回它们。 您必须确保在测试运行时,同一数据库表没有任何队列侦听器/守护程序在运行。

脆性测试 (Brittle Tests)

You might think that snapshot tests are brittle. Just as brittle as poorly written interface tests. Only slightly more brittle than well-written interface tests. And you’d be right.

您可能会认为快照测试很脆弱。 和编写不良的接口测试一样脆弱。 仅比编写良好的接口测试更脆弱。 而且你会是对的。

The question we should be asking is why brittle tests are a problem. An application with some useful tests is in a better place than an application with no tests at all. But when the tests are brittle then any refactoring will break them. Even inconsequential or cosmetic changes are likely to break the tests. Changing the class of a div from “some-style” to “some–style”? If you’re using that in a CSS selector then it’s likely to break the tests.

我们应该问的问题是,为什么脆性测试会成为问题。 具有一些有用测试的应用程序比根本没有测试的应用程序更好。 但是,如果测试很脆弱,那么任何重构都会破坏它们。 即使是无关紧要或表面上的更改也可能破坏测试。 将div的类别从“某些样式”更改为“某些样式”? 如果在CSS选择器中使用它,则很可能会破坏测试。

The problem with brittle tests is that rewriting tests takes time. And we don’t want to rewrite tests all the time, especially when tiny changes have made them break prematurely.

脆弱测试的问题在于重写测试需要时间。 而且我们不想一直重写测试,特别是当微小的更改使测试过早损坏时。

But we’re not actually writing any tests with Snapshot Testing. Not really. Sure, we can write unit tests and integration tests to compliment our Snapshot tests. But when we write these tests we’re just taking something serializable and comparing it to what it was serialized as in the first place. And when they break, after we’re sure nothing has actually broken, we can just delete the snapshot and try again.

但是我们实际上并没有使用Snapshot Testing编写任何测试 。 并不是的。 当然,我们可以编写单元测试和集成测试来补充我们的快照测试。 但是,当我们编写这些测试时,我们只是在获取可序列化的内容,并将其与最初进行序列化的内容进行比较。 当它们中断时,在我们确定没有任何实际中断之后,我们可以删除快照,然后重试。

In a sense, snapshot testing makes non-TDD testing more interactive. It’s OK that these tests are brittle because they’re easy to regenerate, and still useful in many ways.

从某种意义上说,快照测试使非TDD测试更具交互性。 可以肯定的是,这些测试很脆弱,因为它们易于重新生成,并且在许多方面仍然有用。

摘要 (Summary)

I had a wonderful ah-ha moment when I first learned about Snapshot Testing. Since then, most of the front-end tests I write are snapshot tests. It’s been interesting considering use-cases for it in PHP. I hope you’re as excited to dig into it as I am!

当我第一次了解快照测试时,我度过了一个美好的时光。 从那时起,我编写的大多数前端测试都是快照测试。 考虑PHP中的用例很有趣。 希望您像我一样兴奋地深入其中!

Can you think of more places where this would be useful? Tell us about them in the comments!

您能想到更多有用的地方吗? 在评论中告诉我们有关它们的信息!

Thanks, Paul Jones, for pointing me towards Characterization Testing.

谢谢保罗·琼斯(Paul Jones)向我介绍特性测试。

翻译自: https://www.sitepoint.com/snapshot-testing-viable-php/

快照测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值