-
Notifications
You must be signed in to change notification settings - Fork 44
Open
Description
Hello - thanks for a great testing library!
I'm trying to use gopter to test a simple stateful object. The object is initialized with no existing state, and then methods are called. After each method is called, I'd like to execute a check on the internal state of the object. Note this means I don't want to maintain a separate State object, I'd basically like my SystemUnderTest to also act as the State object and I don't want to have to do any kind of updates in NextState. Ideally the Run method will be called, executing one of the object's methods, and then PostCondition will check some property of the object.
I believe I managed to get this working, but a few things that aren't clear:
- Why does InitialStateGen run so many times? I would expect it to run once for each set of commands being executed but it seems to run much more than that.
- Is it wrong to try and use the commands.SystemUnderTest as the commands.State? If I try to do so, it doesn't seem to work, since the commands.State and the commands.SystemUnderTests objects seem to come from different calls to InitialStateGen, so the commands.State doesn't see the changes effected to the commands.SystemUnderTest during calls to Run. I can work around this by returning the SystemUnderState as the result in Run and then checking the result in PostCondition while just ignoring the State. Is that the right thing to do? Related Stateful testing: Compare SystemUnderTest to State? #25
I've included a minimum viable example below.
Thanks!
package keeper
import (
"fmt"
"testing"
"github.com/leanovate/gopter"
"github.com/leanovate/gopter/commands"
"github.com/leanovate/gopter/gen"
)
type sys struct {
height int
heights map[int]uint8
}
// store i under the latest height value and incremenet the height value
func (s *sys) New(i uint8) {
h := s.height
s.height += 1
// bug when height is 5
if s.height == 5 {
return
}
s.heights[h] = i
}
type newCommand uint8
func (value newCommand) Run(q commands.SystemUnderTest) commands.Result {
s := q.(*sys)
s.New(uint8(value))
return s
}
// expect there to be an entry in the `sys.heights` map for each height up to the `sys.height`
func (newCommand) PostCondition(state commands.State, result commands.Result) *gopter.PropResult {
// XXX: if I try to use the state instead of the result here, it doesn't work
// (state doesn't seem to see the effects from Run)
s := result.(*sys)
for i := 0; i < s.height; i++ {
_, ok := s.heights[i]
if !ok {
return &gopter.PropResult{Status: gopter.PropFalse}
}
}
return &gopter.PropResult{Status: gopter.PropTrue}
}
func (value newCommand) NextState(state commands.State) commands.State {
s := state.(*sys)
return s
}
func (newCommand) PreCondition(state commands.State) bool {
return true
}
func (value newCommand) String() string {
return fmt.Sprintf("New(%d)", value)
}
var genNewCommand = gen.UInt8().Map(func(value uint8) commands.Command {
return newCommand(value)
})
func cbCommands(t *testing.T) *commands.ProtoCommands {
return &commands.ProtoCommands{
NewSystemUnderTestFunc: func(initState commands.State) commands.SystemUnderTest {
return initState
},
InitialStateGen: func(p *gopter.GenParameters) *gopter.GenResult {
// XXX: this function seems to execute many more times than Run
result := &sys{
height: 0,
heights: make(map[int]uint8),
}
return gopter.NewGenResult(result, gopter.NoShrinker)
},
GenCommandFunc: func(state commands.State) gopter.Gen {
return gen.OneGenOf(genNewCommand)
},
}
}
func TestSys(t *testing.T) {
parameters := gopter.DefaultTestParametersWithSeed(1235)
properties := gopter.NewProperties(parameters)
properties.Property("circular buffer", commands.Prop(cbCommands(t)))
properties.TestingRun(t)
}Metadata
Metadata
Assignees
Labels
No labels