golang实现Jest风格快照测试的插件库go-snaps的使用

Golang实现Jest风格快照测试的插件库go-snaps的使用

安装

使用go get安装go-snaps:

go get github.com/gkampitakis/go-snaps

在代码中导入go-snaps/snaps包:

package example

import (
  "testing"

  "github.com/gkampitakis/go-snaps/snaps"
)

func TestExample(t *testing.T) {
  snaps.MatchSnapshot(t, "Hello World")
}

MatchSnapshot

MatchSnapshot可以捕获任何类型的结构化或非结构化数据。

你可以传递多个参数给MatchSnapshot或者在同一个测试中多次调用MatchSnapshot。区别在于后者会在快照文件中创建多个条目。

// test_simple.go

func TestSimple(t *testing.T) {
  t.Run("should make multiple entries in snapshot", func(t *testing.T) {
    snaps.MatchSnapshot(t, 5, 10, 20, 25)
    snaps.MatchSnapshot(t, "some value")
  })
}

go-snaps将快照保存在__snapshots__目录中,文件名是测试文件名加上.snap扩展名。

例如,如果你的测试文件名为test_simple.go,当你运行测试时,会在./__snapshots__/test_simple.snaps创建一个快照文件。

MatchStandaloneSnapshot

MatchStandaloneSnapshot会在单独的文件中创建快照,而MatchSnapshot会在同一个文件中添加多个快照。

// test_simple.go

func TestSimple(t *testing.T) {
  snaps.MatchStandaloneSnapshot(t, "Hello World")
  // 或创建一个html快照文件
  snaps.WithConfig(snaps.Ext(".html")).
    MatchStandaloneSnapshot(t, "<html><body><h1>Hello World</h1></body></html>")
}

go-snaps将快照保存在__snapshots__目录中,文件名是t.Name()加上一个数字和.snap扩展名。

对于上面的例子,快照文件名将是./__snapshots__/TestSimple_1.snap./__snapshots__/TestSimple_1.snap.html

MatchJSON

MatchJSON可用于捕获可以表示有效json的数据。

你可以传递一个有效的json,形式为string[]byte,或者任何可以成功传递给json.Marshal的值。

func TestJSON(t *testing.T) {
  type User struct {
    Age   int
    Email string
  }

  snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
  snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`))
  snaps.MatchJSON(t, User{10, "mock-email"})
}

JSON将以漂亮的格式保存在快照中,以提高可读性和确定性差异。

MatchStandaloneJSON

MatchStandaloneJSON会在单独的文件中创建快照,而MatchJSON会在同一个文件中添加多个快照。

func TestSimple(t *testing.T) {
  snaps.MatchStandaloneJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`)
  snaps.MatchStandaloneJSON(t, User{10, "mock-email"})
}

go-snaps将快照保存在__snapshots__目录中,文件名是t.Name()加上一个数字和.snap.json扩展名。

对于上面的例子,快照文件名将是./__snapshots__/TestSimple_1.snap.json./__snapshots__/TestSimple_2.snap.json

MatchYAML

MatchYAML可用于捕获可以表示有效yaml的数据。

你可以传递一个有效的yaml,形式为string[]byte,或者任何可以成功传递给yaml.Marshal的值。

func TestYAML(t *testing.T) {
  type User struct {
    Age   int
    Email string
  }

  snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com")
  snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com"))
  snaps.MatchYAML(t, User{10, "mock-email"})
}

MatchStandaloneYAML

MatchStandaloneYAML会在单独的文件中创建快照,而MatchYAML会在同一个文件中添加多个快照。

func TestSimple(t *testing.T) {
  snaps.MatchStandaloneYAML(t, "user: \"mock-user\"\nage: 10\nemail: \"mock@email.com\"")
  snaps.MatchStandaloneYAML(t, User{10, "mock-email"})
}

go-snaps将快照保存在__snapshots__目录中,文件名是t.Name()加上一个数字和.snap.yaml扩展名。

对于上面的例子,快照文件名将是./__snapshots__/TestSimple_1.snap.yaml./__snapshots__/TestSimple_2.snap.yaml

匹配器

MatchJSONMatchYAML的第三个参数可以接受一个匹配器列表。匹配器是可以作为属性匹配器和测试值的函数。

你可以传递要匹配和测试的属性的路径。

目前go-snaps有三个内置匹配器:

  • match.Any
  • match.Custom
  • match.Type[ExpectedType]

match.Any

Any匹配器充当任何值的占位符。它将任何目标路径替换为占位符字符串。

Any("user.name")
// 或多个路径
Any("user.name", "user.email")

Any匹配器提供了一些设置选项的方法:

match.Any("user.name").
  Placeholder(value). // 允许定义一个不同于默认"<Any Value>"的占位符值
  ErrOnMissingPath(bool) // 确定匹配器在路径缺失时是否会出错,默认为true

match.Custom

自定义匹配器允许你带来自己的验证和占位符值。

match.Custom("user.age", func(val any) (any, error) {
    age, ok := val.(float64)
    if !ok {
        return nil, fmt.Errorf("expected number but got %T", val)
    }

    return "some number", nil
})

如果自定义匹配器返回错误,快照测试将失败并显示该错误。

自定义匹配器提供了一个设置选项的方法:

match.Custom("path",myFunc).
  Placeholder(value). // 允许定义一个不同于默认"<Any Value>"的占位符值
  ErrOnMissingPath(bool) // 确定匹配器在路径缺失时是否会出错,默认为true

match.Type

Type匹配器评估快照中传递的类型,并将任何目标路径替换为<Type:ExpectedType>形式的占位符。

match.Type[string]("user.info")
// 或多个路径
match.Type[float64]("user.age", "data.items")

Type匹配器提供了一个设置选项的方法:

match.Type[string]("user.info").
  ErrOnMissingPath(bool) // 确定匹配器在路径缺失时是否会出错,默认为true

配置

go-snaps允许传递配置来覆盖:

  • 存储快照的目录,相对或绝对路径
  • 存储快照的文件名
  • 快照文件的扩展名(无论扩展名如何,文件名都将在文件名中包含.snaps)
  • 以编程方式控制是否更新快照
  • json配置的json格式配置:
    • Width: 换行json输出前的最大字符宽度(默认:80)
    • Indent: 用于嵌套结构的缩进字符串(默认:1个空格)
    • SortKeys: 是否按字母顺序排序json对象键(默认:true)
t.Run("snapshot tests", func(t *testing.T) {
  snaps.WithConfig(snaps.Filename("my_custom_name"), snaps.Dir("my_dir")).MatchSnapshot(t, "Hello Word")

  s := snaps.WithConfig(
    snaps.Dir("my_dir"),
    snaps.Filename("json_file"),
    snaps.Ext(".json"),
    snaps.Update(false),
    snaps.JSON(snaps.JSONConfig{
      Width:    80,
      Indent:   "    ",
      SortKeys: false,
    }),
  )

  s.MatchJSON(t, `{"hello":"world"}`)
})

更新快照

你可以通过将UPDATE_SNAPS环境变量设置为true来更新失败的快照。

UPDATE_SNAPS=true go test ./...

如果你不想更新所有失败的快照,或者只想更新其中一个快照,你可以使用-run标志来定位你想要的测试。

清理过时的快照

go-snaps可以识别过时的快照。

要启用此功能,你需要在测试运行后使用TestMain(m *testing.M)调用snaps.Clean(t)。这也会打印一个快照摘要(如果使用详细标志-v运行测试)。

如果你想删除过时的快照文件和快照,你可以运行带有UPDATE_SNAPS=clean环境变量的测试。

使用TestMain的原因是go-snaps需要确保所有测试都已完成,以便它可以跟踪哪些快照未被调用。

示例:

func TestMain(m *testing.M) {
  v := m.Run()

  // 在所有测试运行后,`go-snaps`可以检查未使用的快照
  snaps.Clean(m)

  os.Exit(v)
}

排序快照

默认情况下,go-snaps将新快照附加到快照文件中,在并行测试的情况下顺序是随机的。如果你希望快照按确定性顺序排序,你需要为每个包使用TestMain:

func TestMain(m *testing.M) {
  v := m.Run()

  // 在所有测试运行后,`go-snaps`将对快照进行排序
  snaps.Clean(m, snaps.CleanOpts{Sort: true})

  os.Exit(v)
}

跳过测试

如果你想使用t.Skip跳过一个测试,go-snaps无法跟踪测试是被跳过还是被移除。因此,go-snaps暴露了t.Skipt.Skipft.SkipNow的包装器,它们会跟踪跳过的文件。

你可以通过使用-run标志来跳过或仅运行特定的测试。go-snaps可以识别哪些测试被跳过,并仅解析相关测试以查找过时的快照。

在CI上运行测试

go-snaps检测到它在CI上运行时,如果快照缺失或有差异,它将自动失败。这是为了确保新的快照与测试一起提交,并且断言成功。

你可以通过将UPDATE_SNAPS设置为always来覆盖此行为,这将创建或更新快照。

go-snaps使用ciinfo来检测是否在CI环境中运行。

无颜色

go-snaps支持通过设置NO_COLOR环境变量为任何值来禁用彩色输出。

NO_COLOR=true go test ./...

快照结构

快照的形式为:

[TestName - Number]
<data>
---

TestID是测试名称加上一个递增的数字,以允许在单个测试中多次调用MatchSnapshot

[TestSimple/should_make_a_map_snapshot - 1]
map[string]interface{}{
    "mock-0": "value",
    "mock-1": int(2),
    "mock-2": func() {...},
    "mock-3": float32(10.399999618530273),
}
---

注意:如果你的快照数据在一行的开头包含字符---后跟一个新行,go-snaps将"转义"它们并将它们保存为/-/-/-/,以将它们与终止字符区分开来。

已知限制

  • 当通过指定路径运行特定的测试文件go test ./my_test.go时,go-snaps无法跟踪路径,因此会错误地将快照标记为过时。
  • go-snaps不处理CRLF行尾。如果你使用Windows,你可能需要将行尾转换为LF。
  • 当使用go test -trimpath ./...运行时,go-snaps无法自动确定快照路径。然后它依赖于当前工作目录来定义快照目录。如果这在你的用例中是一个问题,你可以使用snaps.WithConfig(snaps.Dir("/some/absolute/path"))设置一个绝对路径

致谢

这个库使用了Jest Snapshoting和Cupaloy作为灵感。

  • Jest是一个成熟的Javascript测试框架,具有强大的快照功能。
  • Cupaloy是一个很棒且简单的Golang快照解决方案。
  • 徽标由MariaLetta制作。

更多关于golang实现Jest风格快照测试的插件库go-snaps的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现Jest风格快照测试的插件库go-snaps的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


使用go-snaps实现Jest风格的快照测试

在Go语言中实现类似Jest风格的快照测试,可以使用go-snaps这个库。它提供了简单直观的API来创建和比较快照,非常适合组件测试和数据结构的验证。

安装go-snaps

首先安装go-snaps库:

go get github.com/gkampitakis/go-snaps

基本使用示例

package main

import (
	"testing"
	
	"github.com/gkampitakis/go-snaps"
)

func TestUserStruct(t *testing.T) {
	user := struct {
		Name  string
		Age   int
		Email string
	}{
		Name:  "John Doe",
		Age:   30,
		Email: "john@example.com",
	}

	// 创建或比较快照
	snaps.MatchSnapshot(t, user)
}

主要功能

1. 基本快照测试

func TestBasicTypes(t *testing.T) {
	t.Run("test string", func(t *testing.T) {
		snaps.MatchSnapshot(t, "hello world")
	})

	t.Run("test number", func(t *testing.T) {
		snaps.MatchSnapshot(t, 42)
	})

	t.Run("test slice", func(t *testing.T) {
		snaps.MatchSnapshot(t, []string{"go", "rust", "python"})
	})
}

2. 结构化数据测试

func TestComplexStructure(t *testing.T) {
	data := map[string]interface{}{
		"user": struct {
			ID    int
			Name  string
			Roles []string
		}{
			ID:    1,
			Name:  "Alice",
			Roles: []string{"admin", "user"},
		},
		"settings": map[string]bool{
			"notifications": true,
			"dark_mode":     false,
		},
	}

	snaps.MatchSnapshot(t, data)
}

3. 测试配置

可以在TestMain中配置全局行为:

func TestMain(m *testing.M) {
	v := m.Run()

	// 在测试结束后检查是否有未使用的快照
	snaps.Clean(m, snaps.CleanOpts{AfterTestRun: true})

	os.Exit(v)
}

高级用法

1. 自定义快照名称

func TestWithCustomName(t *testing.T) {
	result := "custom snapshot content"
	snaps.MatchSnapshot(t, result, "custom_snapshot_name")
}

2. 更新快照模式

当需要更新快照时,可以设置环境变量:

UPDATE_SNAPS=true go test ./...

或者在代码中控制:

func TestWithUpdate(t *testing.T) {
	t.Setenv("UPDATE_SNAPS", "true")
	
	// 现在运行测试会更新快照而不是比较
	snaps.MatchSnapshot(t, "updated content")
}

3. 忽略特定字段

func TestWithIgnoredFields(t *testing.T) {
	user := struct {
		Name    string
		Age     int
		Created time.Time // 这个字段我们希望忽略
	}{
		Name:    "Bob",
		Age:     25,
		Created: time.Now(),
	}

	// 忽略Created字段
	snaps.MatchSnapshot(t, user, snaps.WithIgnoreFields("Created"))
}

最佳实践

  1. 快照文件管理

    • 快照文件默认存储在__snapshots__目录中
    • 应该将这些文件提交到版本控制中
  2. 测试稳定性

    • 避免在快照中包含动态数据(如时间戳、随机数)
    • 对于不可避免的动态数据,使用WithIgnoreFields忽略它们
  3. 测试粒度

    • 每个测试用例应该有明确的快照
    • 避免一个快照包含太多不相关的数据
  4. CI集成

    • 在CI环境中确保不设置UPDATE_SNAPS环境变量
    • 快照测试失败应该导致构建失败

与标准测试库的比较

相比标准库的testing包,go-snaps提供了:

  1. 更简单的数据结构验证方式
  2. 更好的差异可视化
  3. 自动快照更新功能
  4. 更直观的错误报告

总结

go-snaps为Go语言带来了类似Jest的快照测试体验,特别适合验证复杂数据结构、API响应和组件渲染输出。它的简单API和强大功能可以显著减少编写和维护测试代码的工作量。

通过合理使用快照测试,你可以快速构建可靠的测试套件,同时保持测试代码的简洁性和可维护性。

回到顶部