JavaScript 扩展
利用 Go 的速度、能力和效率,同时提供在测试脚本中使用 JavaScript API 的灵活性。
通过实现 k6 接口,您可以弥合测试设置中的各种差距
- 新的网络协议
- 性能提升
- k6 核心版不支持的功能
开始之前
要运行本教程,您需要安装以下应用
- Go
- Git
您还需要安装 xk6
go install go.k6.io/xk6/cmd/xk6@latest
编写一个简单的扩展
- 首先,设置一个工作目录
mkdir xk6-compare; cd xk6-compare; go mod init xk6-compare
- 在该目录中,为您的 JavaScript 扩展创建一个 Go 文件。
一个简单的 JavaScript 扩展需要一个结构体,该结构体暴露供测试脚本调用的方法。
package compare
import "fmt"
// Compare is the type for our custom API.
type Compare struct{
ComparisonResult string // textual description of the most recent comparison
}
// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
func (c *Compare) IsGreater(a, b int) bool {
if a > b {
c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
return true
} else {
c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
return false
}
}
- 注册模块以便从 k6 测试脚本中使用这些方法。
注意
k6 扩展必须带有
k6/x/
前缀,并且在同一 k6 二进制文件中构建的所有扩展中,短名称必须唯一。
import "go.k6.io/k6/js/modules"
// init is called by the Go runtime at application startup.
func init() {
modules.Register("k6/x/compare", new(Compare))
}
- 将文件保存为类似
compare.go
的名称。最终代码如下所示
package compare
import (
"fmt"
"go.k6.io/k6/js/modules"
)
// init is called by the Go runtime at application startup.
func init() {
modules.Register("k6/x/compare", new(Compare))
}
// Compare is the type for our custom API.
type Compare struct{
ComparisonResult string // textual description of the most recent comparison
}
// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
func (c *Compare) IsGreater(a, b int) bool {
if a > b {
c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
return true
} else {
c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
return false
}
}
编译您的扩展 k6
要使用此扩展构建 k6 二进制文件,运行此命令
xk6 build --with xk6-compare=.
注意
从源代码构建时,
xk6-compare
是传递给go mod init
的 Go 模块名称。通常,这将是一个类似于github.com/grafana/xk6-compare
的 URL。
使用您的扩展
现在,在测试脚本中使用该扩展!
- 创建一个名为
test.js
的文件,然后添加以下代码
import compare from 'k6/x/compare';
export default function () {
console.log(`${compare.isGreater(2, 1)}, ${compare.comparison_result}`);
console.log(`${compare.isGreater(1, 3)}, ${compare.comparison_result}`);
}
- 使用
./k6 run test.js
运行测试。
它应该输出以下内容
INFO[0000] true, 2 is greater than 1 source=console
INFO[0000] false, 1 is NOT greater than 3 source=console
使用高级模块 API
假设您的扩展需要访问 k6 内部对象,例如,在执行期间检查测试状态。我们将需要对上面的示例进行稍微复杂的更改。
我们的主 Compare
结构体应实现 modules.Instance
接口,以访问 modules.VU
,进而检查 k6 内部对象,例如
lib.State
,包含 VU ID 和迭代次数等值的 VU 状态sobek.Runtime
,VU 使用的 JavaScript 运行时- 一个全局
context.Context
,包含诸如lib.ExecutionState
的对象
此外,应该有一个根模块实现 modules.Module
接口,作为每个 VU 的 Compare
实例的工厂。
注意
重要性取决于您的模块大小。
这看起来像这样
package compare
import (
"fmt"
"go.k6.io/k6/js/modules"
)
// init is called by the Go runtime at application startup.
func init() {
modules.Register("k6/x/compare", New())
}
type (
// RootModule is the global module instance that will create module
// instances for each VU.
RootModule struct{}
// ModuleInstance represents an instance of the JS module.
ModuleInstance struct {
// vu provides methods for accessing internal k6 objects for a VU
vu modules.VU
// comparator is the exported type
comparator *Compare
}
)
// Ensure the interfaces are implemented correctly.
var (
_ modules.Instance = &ModuleInstance{}
_ modules.Module = &RootModule{}
)
// New returns a pointer to a new RootModule instance.
func New() *RootModule {
return &RootModule{}
}
// NewModuleInstance implements the modules.Module interface returning a new instance for each VU.
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{
vu: vu,
comparator: &Compare{vu: vu},
}
}
// Compare is the type for our custom API.
type Compare struct{
vu modules.VU // provides methods for accessing internal k6 objects
ComparisonResult string // textual description of the most recent comparison
}
// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
func (c *Compare) IsGreater(a, b int) bool {
if a > b {
c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
return true
} else {
c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
return false
}
}
// Exports implements the modules.Instance interface and returns the exported types for the JS module.
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{
Default: mi.comparator,
}
}
注意
请注意,我们实现了 Module API,现在
modules.Register
注册的是根模块,而不是我们的 Compare 对象!
访问运行时状态
目前,我们已从 Compare
类型提供了对 modules.VU
的访问;但是,我们尚未利用提供的方法。以下是一个如何利用运行时状态的人为示例
// InternalState holds basic metadata from the runtime state.
type InternalState struct {
ActiveVUs int64 `js:"activeVUs"`
Iteration int64
VUID uint64 `js:"vuID"`
VUIDFromRuntime sobek.Value `js:"vuIDFromRuntime"`
}
// GetInternalState interrogates the current virtual user for state information.
func (c *Compare) GetInternalState() *InternalState {
state := c.vu.State()
ctx := c.vu.Context()
es := lib.GetExecutionState(ctx)
rt := c.vu.Runtime()
return &InternalState{
VUID: state.VUID,
VUIDFromRuntime: rt.Get("__VU"),
Iteration: state.Iteration,
ActiveVUs: es.GetCurrentlyActiveVUsCount(),
}
}
创建一个测试脚本以利用新的 getInternalState()
函数,如下所示
import compare from 'k6/x/compare';
export default function () {
const state = compare.getInternalState();
console.log(
`Active VUs: ${state.activeVUs}, Iteration: ${state.iteration}, VU ID: ${state.vuID}, VU ID from runtime: ${state.vuIDFromRuntime}`
);
}
执行脚本 ./k6 run test-state.js --vus 2 --iterations 5
将产生类似于以下内容的输出
INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 2, VU ID from runtime: 2 source=console
INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 1, VU ID from runtime: 1 source=console
INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 2, VU ID from runtime: 2 source=console
INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 1, VU ID from runtime: 1 source=console
INFO[0000] Active VUs: 2, Iteration: 2, VU ID: 2, VU ID from runtime: 2 source=console
有关此 API 的更广泛使用示例,请查看
k6/execution
模块。
注意事项
default
函数(或由exec
指定的其他函数)中的代码将在测试运行期间多次执行,并且可能由数千个 VU 并行执行。因此,您的扩展的任何操作都应具有高性能并是线程安全的。- 任何繁重的初始化应尽可能在初始化上下文中完成,而不是作为
default
函数执行的一部分。 - 使用注册表的
NewMetric
方法创建自定义指标;要发送它们,使用metrics.PushIfNotDone()
。
有疑问?欢迎加入 k6 社区论坛 的扩展讨论。
接下来,创建一个输出扩展,将测试指标发布到 k6 尚不支持的目标。