assertly

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 24, 2020 License: Apache-2.0 Imports: 13 Imported by: 13

README

Data structure testing library (assertly)

Data structure testing library for Go. GoDoc

This library is compatible with Go 1.10+

Please refer to CHANGELOG.md if you encounter breaking changes.

Introduction

This library enables complex data structure testing, specifically:

  1. Realtime transformation or casting of incompatible data types with directives system.
  2. Consistent way of testing of unordered structures.
  3. Contains, Range, RegExp support on any data structure deeph level.
  4. Switch case directive to provide expected value alternatives based on actual switch/case input match.
  5. Macro system enabling complex predicate and expression evaluation, and customization.

Motivation

This library has been created as a way to unify original testing approaches introduced to dsunit and endly

Usage

Complete data validation with concrete types


import(
	"github.com/stretchr/testify/assert"
	"github.com/viant/assertly"
)


func Test_XX(t *testing.T) {
    
   	
   	
   	var actualRecords []*User = //get actual
   	var expectedRecords []*User = //get expected
   	assertly.AssertValues(t, expectedRecords, actualRecords)
   	
   	//or with custom path and testing.T integration
   	validation, err := assertly.Assert(expected, actual, assertly.NewDataPath("/"))
   	assert.EqualValues(t, 0, validation.FailedCount, validation.Report())

   	
}

Partial data validation with directive and reg expression


func Test_XX(t *testing.T) {
    
    var actualConfig = &Config{
        Endpoint: &Endpoint{
            Port: 8080,
            TimeoutMs: 2000,
        },
        LogTypes: map[string]*LogType{
            "type1": &LogType{
                Locations:[]*Location{
                    {
                        URL:"file:///data/log/type1",
                    },
                },
                MaxQueueSize: 2048,
                QueueFlashCount: 1024,
                FlushFrequencyInMs: 500,
            },
            "type2":  &LogType{
                Locations:[]*Location{
                    {
                        URL:"file:///data/log/type2",
                    },
                },
                MaxQueueSize: 4096,
                QueueFlashCount: 2048,
                FlushFrequencyInMs: 1000,
            },
        },
    }
                           
       
    var expectedConfig = expected: `{
      "Endpoint": {
        "Port": 8080,
        "TimeoutMs": 2000
      },
      "LogTypes": {
        "type1": {
          "Locations":[
            {
              "URL":"~/type1/"
            }
          ],
          "MaxQueueSize": 2048,
          "QueueFlashCount": 1024,
          "FlushFrequencyInMs": 500
        },
        "@exists@type2": true 
      }
    }`,
    
    assertly.AssertValues(t, expectedConfig, actualConfig)
   	
}

  • reg expression: "URL":"~/type1/"
  • directive: @exists@

Validation with custom macro value provider


type fooProvider struct{}

func (*fooProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) {
	var args = []string{}
	for _, arg := range arguments {
		args = append(args, toolbox.AsString(arg))
	}
	return fmt.Sprintf("foo{%v}", strings.Join(args, ",")), nil
}

func Test_XX(t *testing.T) {
	ctx := NewDefaultContext()
	var provider toolbox.ValueProvider = &fooProvider{}
	ctx.Evaluator.ValueProviderRegistry.Register("foo", provider)

	var actual = map[string]string{
		"k1":"v1",
		"k2":"Macro test: foo{1,abc} !",
	}
	
	var expected = map[string]string{
		"k1":"v1",
		"k2":"Macro test: <ds:foo[1,\"abc\"]> !",
	}


	AssertValuesWithContext(ctx, t, expected, actual)
}

Validation with custom predicate

type rangePredicate struct {
	min int
	max int
	actual int
	err error
}

func (p *rangePredicate) String() string {
	return fmt.Sprintf("min: %v, max: %v, actual: %v, err: %v", p.min, p.max, p.actual, p.err)
}

func (p *rangePredicate) Apply(value interface{}) bool {
	p.actual, p.err = toolbox.ToInt(value)
	return p.actual >= p.min && p.actual <= p.max
}



type inRangePredicateProvider struct{}
func (*inRangePredicateProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) {
	if len(arguments) != 2 {
		return nil, fmt.Errorf("expected 2 arguments (min, max) but had: %v", len(arguments))
	}
	min, err := toolbox.ToInt(arguments[0])
	if err != nil {
		return nil, fmt.Errorf("invalid min %v", err)
	}
	max, err := toolbox.ToInt(arguments[1])
	if err != nil {
		return nil, fmt.Errorf("invalid min %v", err)
	}
	var predicate toolbox.Predicate =  &rangePredicate{min:min, max: max}
	return &predicate, nil
}



func Test_XX(t *testing.T) {
	ctx := NewDefaultContext()
	var provider toolbox.ValueProvider = &inRangePredicateProvider{}
	ctx.Evaluator.ValueProviderRegistry.Register("inRange", provider)


	var actual = map[string]int{
		"k1":1,
		"k2":3,
	}


	var expected = map[string]string{
		"k1":"1",
		"k2":"<ds:inRange[2,10]>",
	}


	AssertValuesWithContext(ctx, t, expected, actual)
}

Validation

Validation rules:

  1. JSON textual data is converted into data structure
  2. New Line Delimited JSON is converted into data structure collection.
  3. Object/Struct is converted into data structure
  4. Only existing keys/fields in expected data structure are validated
  5. Only existing items in the array/slice are validated
  6. Directive and macros/predicate provide validation extension
  7. The following expression can be used on any data structure level:
Assertion Type input expected expression example
equal actual expected a:a
not equal actual !expected a:!b
contains actual /expected/ abcd:/bc/
not contains actual !/expected/ abcd:!/xc/
regExpr actual ~/expected/ 1234a:/\d+/
not regExpr actual !~/expected/ 1234:!/\w/
between actual /[minExpected..maxExpected]/ 12:/[1..13]/
exists n/a { "key": "@exists@" }
not exists n/a { "key": "@!exists@" }

example:


func Test_XX(t *testing.T) {
    
var expected = `
{
  "Meta": "abc",
  "Table": "/table_/",
  "Rows": [
    {
      "id": 1,
      "name": "~/name (\\d+)/",
      "@exists@":"dob"
    },
    {
      "id": 2,
      "name": "name 2",
      "settings": {
        "k1": "v2"
      }
    },
    {
      "id": 2,
      "name": "name 2"
    }
  ]
}`,
var actual = `
{
  "Table": "table_xx",
  "Rows": [
    {
      "id": 1,
      "name": "name 12",
      "dob":"2018-01-01"
    },
    {
      "id": 2,
      "name": "name 2",
      "settings": {
        "k1": "v20"
      }
    },
    {
      "id": 4,
      "name": "name 2"
    }
  ]
}`,
	
    validation, err := assertly.Assert(expected, actual, assertly.NewDataPath("/"))
   	assert.EqualValues(t, 0, validation.FailedCount, validation.Report())
}


Directive

Directive is piece of information instructing validator to either convert data just before validation takes place or to validate a date according to provided rules.

  • KeyExistsDirective = "@exists@"
  • KeyDoesNotExistsDirective = "@!exists@"
  • TimeFormatDirective = "@timeFormat@"
  • TimeLayoutDirective = "@timeLayout@"
  • SwitchByDirective = "@switchCaseBy@"
  • CastDataTypeDirective = "@cast@"
  • IndexByDirective = "@indexBy@"
  • CaseSensitiveDirective = "@caseSensitive@"
  • KeyCaseSensitiveDirective = "@CaseSensitive@"
  • NumericPrecisionPointDirective = "@numericPrecisionPoint@"
  • CoalesceWithZeroDirective = "@coalesceWithZero@"
  • AssertPathDirective = "@assertPath@"
  • LengthDirective = "@length@"
  • StrictMapCheckDirective = "@strictMapCheck@"

Assert Path

@assertPath@ directive allows validation only specified path within given node, the following construct can be used:

  • directive prefixed

{
    "@assertPath@Responses[0].Code":200,
    "@assertPath@Responses[1].Code":200   
}

  • directive with subpath and values map
{
    "@assertPath@":{
      "Responses[0].Code":200,
      "Responses[1].Code":200
      }   
}
  • directive with the same data point validation
{
    "@assertPath@":[
        {
          "Responses[0].Code":200,
          "Responses[0].Body":"/some fragment/"
      },
      {
           "Responses[0].Body":"~/.+\\d{3}.+/"
      }   
  ]
}
Index by

@indexBy@ - index by directive indexes a slice for validation process, specifically.

  1. Two unordered array/slice/collection that can be index by a unique fields
  2. A map with a actual array/slice/collection that can be ordered by unique fields

Example 1

#expected

{
"@indexBy@":"id",
"1" :{"id":1, "name":"name1"},
"2" :{"id":2, "name":"name2"}
}

#actual

[
{"id":1, "name":"name1"},
{"id":2, "name":"name2"}
]

Example 2

#expected

{"@indexBy@":"id"}
{"id":1, "name":"name1"}
{"id":2, "name":"name2"}

#actual

{"id":1, "name":"name1"}
{"id":2, "name":"name2"}

Example 3

#expected

{"@indexBy@":"request.id"}
{"request":{"id":1111, "name":"name1"}, "ts":189321233}
{"request":{"id":2222, "name":"name2"}, "ts":189321235}

#actual

{"request":{"id":2222, "name":"name2"}, "ts":189321235}
{"request":{"id":1111, "name":"name1"}, "ts":189321233}

Switch/case

@switchCaseBy@ - switch directive instructs a validator to select matching expected subset based on some actual value. . For non deterministic system there could be various alternative output for the same input.

Example 1

#expected

[
  {
    "@switchCaseBy@":["experimentID"]
  },
  {
    "1":{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]},
    "2":{"experimentID":2, "seq":1, "outcome":[3.53,6.32,3.34]}
  },
  {
    "1":{"experimentID":1, "seq":2, "outcome":[5.63,4.3]},
    "2":{"experimentID":1, "seq":2, "outcome":[3.65,3.2]}
  }
]

#actual

{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]}
{"experimentID":1, "seq":2, "outcome":[5.63,4.3]}

Example 2

#expected

[
  {
    "@switchCaseBy@":["experimentID"]
  },
  {
    "1":{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]},
    "2":{"experimentID":2, "seq":1, "outcome":[3.53,6.32,3.34]},
    "shared": {"k1":"v1", "k2":"v2"}
  },
  {
    "1":{"experimentID":1, "seq":2, "outcome":[5.63,4.3]},
    "2":{"experimentID":1, "seq":2, "outcome":[3.65,3.2]},
    "shared": {"k1":"v10", "k2":"v20"}
  }
]

#actual

{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34], "k1":"v1", "k2":"v2"}
{"experimentID":1, "seq":2, "outcome":[5.63,4.3], "k1":"v10", "k2":"v20"}

Time format

@timeFormat@ - time format directive instructs a validator to convert data into time with specified time format before actual validation takes place.

Time format is expressed in java style date format.

Example

#expected

expected := map[string]interface{}{
    "@timeFormat@date": "yyyy-MM-dd",
    "@timeFormat@ts": "yyyy-MM-dd hh:mm:ss"
    "@timeFormat@" "yyyy-MM-dd hh:mm:ss" //default time format       
    "id":123,
    "date": "2019-01-01",
    "ts": "2019-01-01 12:00:01",
}

#actual

expected := map[string]interface{}{
	"id":123,
    "date": "2019-01-01 12:00:01",,
    "ts": "2019-01-01 12:00:01",
}

Time layout

@timeLayout@ - time format directive instructs a validator to convert data into time with specified time format before actual validation takes place.

Time layout uses golang time layout.

Example

#expected

expected := map[string]interface{}{
    "@timeFormat@date": "yyyy-MM-dd",
    "@timeFormat@ts": "yyyy-MM-dd hh:mm:ss"
    "@timeFormat@" "yyyy-MM-dd hh:mm:ss" //default time format       
    "id":123,
    "date": "2019-01-01",
    "ts": "2019-01-01 12:00:01",
}

#actual

expected := map[string]interface{}{
	"id":123,
    "date": "2019-01-01 12:00:01",,
    "ts": "2019-01-01 12:00:01",
}

Cast data type

@cast@ - instruct a validator to convert data to the specified data type before actual validation takes place.

Supported data type casting:

  • int
  • float
  • boolean

Example

#expected

[
  {
    "@cast@field1":"float","@cast@field2":"int"
  },
  {
       "field1":2.3,
       "field2":123
  },
  {
     "field1":6.3,
     "field2":551
  }
]

#actual

{"field1":"2.3","field2":"123"}
{"field1":"6.3","field2":"551"}

KeyCaseSensitiveDirective

By default map key match is case sensitive, directive allows to disable that behaviours.

CaseSensitiveDirective

By default text value match is case sensitive, directive allows to disable that behaviours.

NumericPrecisionPoint

NumericPrecisionPoint controls numeric precision validation comparision

Example

#expected

[
  {
    "@numericPrecisionPoint@":"7"
  },
  {
      "field1":0.006521405,
       "field2":123
  },
  {
     "field1":0.006521408,
     "field2":551
  }
]

#actual

[
   {
       "field1":0.0065214,
        "field2":123
   },
   {
      "field1":0.0065214,
      "field2":551
   }
]

CoalesceWithZero

Coalesce with zero directive sets all nil numeric values to zero

Length Directive

Checks length or map or slice

Example

#expected

{
"@length@k1":3
}

#actual

   {
       "k1":[1,2,3]
   }

Source directive

Source directive is helper directive providing additional information about data point source, i.e. file.json#L113

Macro and predicates

The macro is an expression with parameters that expands original text value. The general format of macro: <ds:MACRO_NAME [json formated array of parameters]>

The following macro are build-in:

Name Parameters Description Example
env name env variable Returns value env variable <ds:env["user"]>
nil n/a Returns nil value <ds:nil>
cast type name Returns value env variable <ds:cast["int", "123"]>
current_timestamp n/a Returns time.Now() <ds:current_timestamp>
dob user age, month, day, format(yyyy-MM-dd as default) Returns Date Of Birth <ds:dob>

Predicates

Predicate allows expected value to be evaluated with actual data using custom predicate logic.

Name Parameters Description Example
between from, to values Evaluate actual value with between predicate <ds:between[1.888889, 1.88889]>
within_sec base time, delta, optional date format Evaluate if actual time is within delta of the base time <ds:within_sec["now", 6, "yyyyMMdd HH:mm:ss"]>

Example

    expected := `<ds:between[1,10]>`
    actual := 3
    expected := `1<ds:env["USER"]>3`,
    actual := fmt.Sprintf("1%v3", os.Getenv("USER"))
    expected := `<ds:dob[3, 6, 3>`
    actual := 2015-06-03
    expected := `<ds:dob[3, 6, 3,"yyyy-MM-dd"]>`
    actual := 2015-06-03
    expected := `<ds:dob[3, 6, 3,"yyyy"]>`
    actual := 2015
    expected := `<ds:dob[3, 9, 2,"yyyy-MM"]>`
    actual := 2015-09
    expected := `<ds:dob[5, 12, 25,"-MM-dd"]>`
    actual := 12-25

External resource

GoCover

GoCover

License

The source code is made available under the terms of the Apache License, Version 2, as stated in the file LICENSE.

Individual files may be made available under their own specific license, all compatible with Apache License, Version 2. Please see individual files for details.

Credits and Acknowledgements

Library Author: Adrian Witas

Documentation

Index

Constants

View Source
const (
	KeyExistsDirective             = "@exists@"
	KeyDoesNotExistsDirective      = "@!exists@"
	TimeFormatDirective            = "@timeFormat@"
	TimeLayoutDirective            = "@timeLayout@"
	SwitchByDirective              = "@switchCaseBy@"
	CastDataTypeDirective          = "@cast@"
	IndexByDirective               = "@indexBy@"
	KeyCaseSensitiveDirective      = "@keyCaseSensitive@"
	CaseSensitiveDirective         = "@caseSensitive@"
	SourceDirective                = "@source@"
	SortTextDirective              = "@sortText@"
	NumericPrecisionPointDirective = "@numericPrecisionPoint@"
	CoalesceWithZeroDirective      = "@coalesceWithZero@"
	AssertPathDirective            = "@assertPath@"
	LengthDirective                = "@length@"
	StrictMapCheckDirective        = "@strictMapCheck@"
)
View Source
const (
	MissingEntryViolation         = "entry was missing"
	MissingItemViolation          = "item was missing"
	ItemMismatchViolation         = "item was mismatched"
	IncompatibleDataTypeViolation = "data type was incompatible"
	KeyExistsViolation            = "key should exist"
	KeyDoesNotExistViolation      = "key should not exist"
	EqualViolation                = "value should be equal"
	NotEqualViolation             = "value should not be equal"
	LengthViolation               = "should have the same length"
	MissingCaseViolation          = "missing switch/case value"
	RegExprMatchesViolation       = "should match regexpr"
	RegExprDoesNotMatchViolation  = "should not match regexpr"
	RangeViolation                = "should be in range"
	RangeNotViolation             = "should not be in range"
	ContainsViolation             = "should contain fragment"
	DoesNotContainViolation       = "should not contain fragment"
	PredicateViolation            = "should pass predicate"
	ValueWasNil                   = "should have not nil"
	SharedSwitchCaseKey           = "shared"
)

Variables

View Source
var ValueProviderRegistry = toolbox.NewValueProviderRegistry()

ValueProviderRegistry represents value provider ValueProviderRegistry

Functions

func AssertValues

func AssertValues(t *testing.T, expected, actual interface{}, arguments ...interface{}) bool

AssertValues validates expected against actual data structure

func AssertValuesWithContext

func AssertValuesWithContext(context *Context, t *testing.T, expected, actual interface{}, arguments ...interface{}) bool

AssertValuesWithContext validates expected against actual data structure with context

func FormatMessage

func FormatMessage(failure *Failure) string

func NewDefaultMacroEvaluator

func NewDefaultMacroEvaluator() *toolbox.MacroEvaluator

Types

type AssertPath added in v0.3.0

type AssertPath struct {
	SubPath  string
	Expected interface{}
}

type Context

type Context struct {
	toolbox.Context
	Directives *Directives
	Evaluator  *toolbox.MacroEvaluator

	StrictDatTypeCheck bool
}

Context represent validation context

func NewContext

func NewContext(ctx toolbox.Context, directives *Directives, evaluator *toolbox.MacroEvaluator) *Context

NewContext returns a context

func NewDefaultContext

func NewDefaultContext() *Context

NewDefaultContext returns default context

type DataPath

type DataPath interface {
	//MatchingPath returns matching path
	MatchingPath() string

	//Path data path
	Path() string

	//Index creates subpath for supplied index
	Index(index int) DataPath

	//Index creates subpath for supplied key
	Key(key string) DataPath

	//Set source for this path, source may represent detail location of data point
	SetSource(string)

	//Get source from this path
	Source() string

	//Match returns a matched directive for this path
	Match(context *Context) *Directive

	//Match returns a directive for this path
	Directive() *Directive

	//Each traverse each data path node upto parent
	Each(callback func(path DataPath) bool)
}

DataPath represents a dat path

func NewDataPath

func NewDataPath(root string) DataPath

NewDataPath returns a new data path.

type Directive

type Directive struct {
	DataPath
	KeyExists             map[string]bool
	KeyDoesNotExist       map[string]bool
	TimeLayout            string
	KeyCaseSensitive      bool
	CaseSensitive         bool
	StrictMapCheck        bool
	TimeLayouts           map[string]string
	DataType              map[string]string
	Lengths               map[string]int
	SwitchBy              []string
	CoalesceWithZero      bool
	NumericPrecisionPoint *int
	IndexBy               []string
	Source                string
	SortText              bool
	AssertPaths           []*AssertPath
}

Match represents a validation TestDirective

func NewDirective

func NewDirective(path DataPath) *Directive

NewDirective creates a new TestDirective for supplied path

func (*Directive) Add

func (d *Directive) Add(target map[string]interface{})

Add adds by to supplied target

func (*Directive) AddDataType

func (d *Directive) AddDataType(key, value string)

AddDataType adds data type TestDirective

func (*Directive) AddKeyDoesNotExist

func (d *Directive) AddKeyDoesNotExist(key string)

AddKeyDoesNotExist adds key does exist TestDirective

func (*Directive) AddKeyExists

func (d *Directive) AddKeyExists(key string)

AddKeyExists adds key exists TestDirective

func (*Directive) AddSort

func (d *Directive) AddSort(key string)

AddKeyExists adds key exists TestDirective

func (*Directive) AddTimeLayout

func (d *Directive) AddTimeLayout(key, value string)

AddTimeLayout adds time layout TestDirective

func (*Directive) Apply

func (d *Directive) Apply(aMap map[string]interface{}) error

Apply applies TestDirective to supplied map

func (*Directive) ApplyKeyCaseInsensitive added in v0.2.2

func (d *Directive) ApplyKeyCaseInsensitive()

func (*Directive) DefaultTimeLayout

func (d *Directive) DefaultTimeLayout() string

DefaultTimeLayout returns default time layout

func (*Directive) ExtractDataTypes

func (d *Directive) ExtractDataTypes(aMap map[string]interface{})

ExtractDataTypes extracts data from from supplied map

func (*Directive) ExtractDirectives

func (d *Directive) ExtractDirectives(aMap map[string]interface{}) bool

ExtractDirective extract TestDirective from supplied map

func (*Directive) IsDirectiveKey

func (d *Directive) IsDirectiveKey(key string) bool

IsDirectiveKey returns true if key is TestDirective

func (*Directive) IsDirectiveValue

func (d *Directive) IsDirectiveValue(value string) bool

IsDirectiveKey returns true if value is TestDirective

type Directives

type Directives struct {
	*Directive
	PathDirectives map[string]*Directive
}

Directives represent a directive

func NewDirectives

func NewDirectives(directives ...*Directive) *Directives

NewDirectives returns new directives

func (*Directives) Match

func (d *Directives) Match(path DataPath) *Directive

type Failure

type Failure struct {
	Source   string
	Path     string
	Expected interface{}
	Actual   interface{}
	Args     []interface{}
	Reason   string
	Message  string
}

Failure represents a validation failre

func NewFailure

func NewFailure(source, path string, reason string, expected, actual interface{}, args ...interface{}) *Failure

NewFailure creates a new failure

func (*Failure) Index

func (f *Failure) Index() int

func (*Failure) LeafKey

func (f *Failure) LeafKey() string

type TestDirective

type TestDirective map[string]interface{}

TestDirective represents TestDirective record

func IndexBy

func IndexBy(key string) TestDirective

func TimeFormat

func TimeFormat(key, format string) TestDirective

func TimeLayout

func TimeLayout(key, format string) TestDirective

func (TestDirective) Cast

func (r TestDirective) Cast(field, dataType string) TestDirective

func (TestDirective) IndexBy

func (r TestDirective) IndexBy(key string) TestDirective

func (TestDirective) KeyCaseSensitive added in v0.2.2

func (r TestDirective) KeyCaseSensitive() TestDirective

func (TestDirective) SortText

func (r TestDirective) SortText() TestDirective

func (TestDirective) TimeFormat

func (r TestDirective) TimeFormat(key, format string) TestDirective

func (TestDirective) TimeLayout

func (r TestDirective) TimeLayout(key, format string) TestDirective

type Validation

type Validation struct {
	TagID       string
	Description string
	PassedCount int
	FailedCount int
	Failures    []*Failure
}

Validation validation

func Assert

func Assert(expected, actual interface{}, path DataPath) (*Validation, error)

Assert validates expected against actual data structure for supplied path

func AssertWithContext

func AssertWithContext(expected, actual interface{}, path DataPath, context *Context) (*Validation, error)

AssertWithContext validates expected against actual data structure for supplied path and context

func NewValidation

func NewValidation() *Validation

NewValidation returns new validation

func (*Validation) AddFailure

func (v *Validation) AddFailure(failure *Failure)

AddFailure add failure to current violation

func (*Validation) HasFailure

func (v *Validation) HasFailure() bool

HasFailure returns true if validation has failures

func (*Validation) MergeFrom

func (v *Validation) MergeFrom(source *Validation)

MergeFrom merges failures and passes from source

func (*Validation) Report

func (v *Validation) Report() string

Report returns validation report

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL