Specimen
Yaml-based data-driven testing
Specimen is a yaml data format for data-driven testing. This enforces separation between feature being tested the data used for testing.
It comes with a golang implementation for loading the data, checking its format, running your golang test boxes (called code boxed) and comparing the result with the expected one.
It supports using the FOCUS
and SKIP
flag in the data to run only part of the test data.
Overview
Getting started
To get started, create a directory it/
and the three files it.go
it_test.go
and it_testdata.yaml
. For each file, copy the content of linked section. Finally, run go test
in the it/
directory.
mkdir it
cd it
touch it.go it_test.go it_testdata.yaml
it.go
see Example package codeit_test.go
see Code boxit_testdata.yml
see Yaml Data
Finally:
go mod init it
go mod tidy
go test
You should get an output like this one:
TestIt:
2 slab-s succeeded over 2. (0 failed)
PASS
ok it
Yaml Data
The yaml data file looks like this:
content:
-
box: turn_page
case:
-
flag: FOCUS
input:
book:
title: aleph
left_page: 0
size: 90
turn_page_count: 4
-
flag: SKIP
input:
book:
title: aleph
left_page: 0
size: 90
turn_page_count: 4
output:
title: aleph
left_page: 8
size: 90
info:
expected_left_page: 8
-
flag: FOCUS
box: get_page
case:
-
input:
book:
title: aleph
left_page: 44
size: 90
output: 44
The input entry is required and must be a map. The info entry is optional but must be a map if present. Both the input map and the info map are passed to the test box. The output entry may be any value but it must only be present if the box returns a value. The flags are case insensitive, though it is recommanded to use UPPERCASE to make them more visible. The supported flags are FOCUS and SKIP. The flag entry may be present both at the slab level and at the case level.
Code box
A code box is an adapter between the parsed data and the library the code being tested. It takes as input the input map and the optional info map. A code box looks like this:
package it_test
import (
"it"
"testing"
"github.com/ditrit/specimen/go/specimen"
)
func TestIt(t *testing.T) {
specimen.Run(
t,
[]specimen.File{
specimen.ReadLocalFile("it_testdata.yaml"),
},
specimen.CodeboxSet(
specimen.NewCodebox("turn_page", turn_page_box),
specimen.NewCodebox("get_page", get_page_box),
),
)
}
// what a code box can look like
func turn_page_box(s *specimen.S, input map[string]interface{}, info map[string]interface{}) interface{} {
book_data := input["book"].(map[string]interface{})
book := it.Book{
Title: book_data["title"].(string),
LeftPage: book_data["left_page"].(int),
Size: book_data["size"].(int),
}
book.TurnPage(input["turn_page_count"].(int))
return map[string]interface{}{
"title": book.Title,
"left_page": book.LeftPage,
"size": book.Size,
}
}
// the output of a code box can be anything
func get_page_box(s *specimen.S, input map[string]interface{}, info map[string]interface{}) interface{} {
book_data := input["book"].(map[string]interface{})
book := it.Book{
Title: book_data["title"].(string),
LeftPage: book_data["left_page"].(int),
Size: book_data["size"].(int),
}
return book.GetPage()
}
// note: if no output comparison is expected by the box, the box must return nil
Example package code
package it
type Book struct {
Title string
LeftPage int
Size int
}
func (b *Book) TurnPage(count int) {
b.LeftPage += 2 * count
if b.LeftPage < 0 {
b.LeftPage = 0
} else if b.LeftPage >= b.Size {
b.LeftPage = b.Size - 1
}
}
func (b *Book) GetPage() int {
return b.LeftPage
}
Yaml Schema
The content of a yaml test data file must match the main
rule of the lidy schema below:
main:
_mapFacultative:
flag: string # any test run flag associated with the file
extra: any # any information you need to store. This can be used as a neutral place to store aliased data
_map:
content: {listOf: databox}
databox:
_mapFacultative:
name: string # the yaml name of the databox. It is mentioned in the error report if present
flag: string # any test run flag associated with the databox
extra: any
_map:
box: string # the name of the code box as declared in the code
case: {_listOf: slab} # the data to be passed to the code box
slab:
_mapFacultative:
name: string
flag: string
info: {_mapOf: {string: any}}
output: any
extra: any
_map:
input: {_mapOf: {string: any}}
Several databoxes can contribute their case to the same code box, though each databox contributes only to one code box