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