I have started my Erlang based MUD a few times. The first time, I got bogged down in telnet details. The second time, I got lost building far too many proxy objects and indirections. Then I wasn’t even sure what I’d written was correct or that my changes wouldn’t break something.
I was having a hard time figuring out how to drive out the code. Where do I start?
This time around, I’ve gotten farther than I ever had before. I’m at a stage were I can move my character from one room to another. The perspective this time, I got a chance to drive the code out by running it via the REPL. I found a happy medium between top down and bottom up. This is similar to TDD use to test out an View-Model. My REPL based code had simple functions that mimicked the future texted based input. Seems like a duh now that I’ve said it. It really does feel more like I’m cutting with the grain.
But now, how do I make sure that I’ve got code that works constantly? Especially when I start dealing with more complex behaviors. I want to “record” my REPL sessions while focusing on building contexts since there are so many ways to build up a player and rooms. To do that, I’ve built a BDD micro framework based on E-Unit and a dash of rspec. I call it “test.hrl” and it is all of three macros!
-include_lib("eunit/include/eunit.hrl").
-define(It(Text,Func), {"It " ++ Text, Func}).
-define(It(Text,Setup,Cleanup,Func),
{"It " ++ Text, setup,Setup,Cleanup,Func}).
-define(Describe(Text,Tests),{"Describe " ++ Text, Tests}).
It is based on the fact e-unit can used nested test descriptions to run tests. I just wrote some macros around it to make it fit my preconceptions of what a bdd framework should be like using terms I like.
When used, it looks like this
-module(player_tests).
-include("tests.hrl").
player_test_() ->[
?Describe("Bad Password",
[?It("should return an error",fun setup/0,fun cleanup/1,
?_test(begin ?assertEqual(error, player:login("Tony", "BassPassword"))end))
]),
?Describe("Good Password",
[?It("should have a player proxy",fun setup/0,fun cleanup/1,
?_test(begin Me = player:login("Tony", "Hello"),
?assertEqual({ok,"You aint got jack!"},
Me:inventory())
end))
]),
?Describe("Room Interaction",
[?It("should describe the lobby",fun setup/0, fun cleanup/1,
?_test(begin Me = player:login("Tony", "Hello"),
?assertEqual({ok, "It's a lobby"},
Me:look())
end)),
?It("should move to the kitchen", fun setup/0, fun cleanup/1,
?_test(begin Me = player:login("Tony", "Hello"),
Me:move("north"),
?assertEqual({ok, "It's a kitchen"},
Me:look()) end))])
].
setup() ->
% I don't know why, but I need the print
% to make the kitchen test pass
io:format(""),
stubs:fake_rooms().
cleanup(_Pid) ->
stubs:stop_fake_rooms(),
true.
Here’s my test runner
-module(test_runner).
-export([run/0]).
-include_lib("eunit/include/eunit.hrl").
run() ->
eunit:test([player_tests],[verbose]).
The output looks like this
erl -noshell -pa ebin -s mnesia start -s test_runner run -s init stop
======================== EUnit ========================
module 'player_tests'
Describe Bad Password
It should return an error
player_tests:7: player_test_...ok
[done in 0.016 s]
[done in 0.016 s]
Describe Good Password
It should have a player proxy
player_tests:11: player_test_...ok
[done in 0.015 s]
[done in 0.015 s]
Describe Room Interaction
It should describe the lobby
player_tests:18: player_test_...ok
[done in 0.016 s]
It should move to the kitchen
player_tests:23: player_test_...ok
[done in 0.016 s]
[done in 0.032 s]
[done in 0.063 s]
=======================================================
All 4 tests passed.
This may not be perfect. I may not be the most ergonomic. I know it isn’t. But it’s close enough for me right now. I feel like I can describe what I want in a manner that fits me. Three macros and a slight change in how I view the world means I finally have this project moving forward well. It is truly amazing what just a little code can do.