Up to Main Index Up to Journal for January, 2019 JOURNAL FOR WEDNESDAY 30TH JANUARY, 2019 ______________________________________________________________________________ SUBJECT: Automating testing of player commands DATE: Wed 30 Jan 23:50:37 GMT 2019 I’m going along, minding my own business and rewriting the GET and DROP commands to use the new item search and matching code. Things are working well, the code is flowing and I might even have hummed to myself a bit: >get all ball You get a small green ball. You get a large red ball. You get a large green ball. You get a small red ball. > The fire gently pops and crackles. >drop all ball You drop a small green ball. You drop a large red ball. You drop a large green ball. You drop a small red ball. > Even the fire seems merry. Everything seems to be working as expected. However the code for implementing commands is becoming a little more verbose. Oh well, the code is doing more now. The structure of the code seems to have improved. Fixed a minor bug in the limit checking of the matcher. I need to set up some automated tests for commands. Currently testing consists of making code changes, compiling, running the server, loging in via Telnet and manually testing commands as a player. This is how I’ve always tested WolfMUD — but it’s very inefficient, error prone and time consuming. Ideally I would like to automate the testing without having to use Telnet. What I need to be able to test are commands, their expected output to players and the command’s effect on the state of the game world. For example ‘GET BALL’. Is the correct item removed from the current location? Is the correct item put into the actor’s inventory? Are the correct messages sent to the actor and observers? Are vetoes checked correctly? Are actions, resets and clean ups handled correctly? So I went back to the WHICH command to see what I could do about testing it using the standard Go testing framework. It actually turns out to be relatively simple. The guts of my approach, with a lot of test cases dropped for brevity, look like this: // Setup test world _ = attr.NewThing( attr.NewStart(), attr.NewName("Test room A"), attr.NewDescription("This is a room for testing."), attr.NewInventory( attr.NewThing( attr.NewName("a small green ball"), attr.NewAlias("+SMALL", "+GREEN", "BALL"), attr.NewDescription("This is a small, green rubber ball."), ), attr.NewThing( attr.NewName("a large green ball"), attr.NewAlias("+LARGE", "+GREEN", "BALL"), attr.NewDescription("This is a large, green rubber ball."), ), ), ) // Setup test actor and add to world actor := cmd.NewTestPlayer("an actor") start := (*attr.Start)(nil).Pick() start.Add(actor) start.Enable(actor) const ( smallGreen = "You see a small green ball here.\n" largeGreen = "You see a large green ball here.\n" ) // Test WHICH command for _, test := range []struct { data string want string }{ {"which ball", smallGreen}, {"which all ball", smallGreen + largeGreen}, {"which green ball", smallGreen}, {"which all green ball", smallGreen + largeGreen}, {"which small ball", smallGreen}, {"which all small ball", smallGreen}, {"which 1 ball", smallGreen}, {"which 2 ball", smallGreen + largeGreen}, {"which 1st ball", smallGreen}, {"which 2nd ball", largeGreen}, {"which 3rd ball", "You don't see that many 'BALL' here.\n"}, {"which frog", "You see no 'FROG' here.\n"}, {"which blue frog", "You see no 'BLUE FROG' here.\n"}, {"which green frog", "You see no 'GREEN FROG' here.\n"}, } { t.Run(fmt.Sprintf("%s", test.data), func(t *testing.T) { have := actor.Script(test.data) if have != test.want { t.Errorf("\nhave %+q\nwant %+q", have, test.want) } }) } // Tear down test world for other tests (*attr.Start)(nil).Destroy() Using this approach I can setup a test world and populate it. I can then add players, have them execute commands via Script and get the responses back that are sent to the players. This also works for messages sent to observers, as in the GET example above. I can also inspect items, inventories and anything else that needs checking. Asynchronous events also fire for resets, respawns and clean up. I should also be able to run parallel tests. I’m also going to write benchmarks for the commands which should prove interesting… I’ve created some testing helpers — a testPlayer type that has the Script method used above. This caused a few issues. I wanted to have some types and methods for testing only and that could be used by multiple _test.go files. However, if I put the helpers in a separate file under the ‘cmd_test’ package I couldn’t import them into other tests. If I put them under the ‘cmd’ package then the helper ended up being included in the normal build. It would be nice to have a ‘test’ build constraint and use ‘// +build test’, but there isn’t one. In the end I put the helpers in pkg_test.go but under the package ‘cmd’. That way ending the filename with _test.go keeps the helpers out of the main build and putting them under the ‘cmd’ package other tests can import ‘cmd’ and use the helpers. I’ve also added a Destroy method to the attr.Start type. This is necessary so that the game world and all of its content is disposed of ready for other tests. Has it been worth the time I’m investing in testing? Well I’ve already fixed another bug in the new search and matching code. I also have two odd corner cases. If you specify 0 for a limit, for example ‘WHICH 0 BALL’, it acts just like ‘WHICH ALL BALL’. The same happens if you try ‘WHICH 0TH BALL’. Still wondering if that’s the right behaviour or not. At the moment I’m still fine tuning the tests and the GET/DROP commands, so the changes are not ready for the public dev branch. For now just the search and matching bug fixes have been pushed out. I hope to have all the rest ready in the next few days. -- Diddymus Up to Main Index Up to Journal for January, 2019