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.