Wednesday, September 14, 2011

Living without client

Recently I've written about the possibility to test the features by writing code, that test them. As I've started doing some refactoring in our code, I immediately wanted to use that feature. Let's see how it worked out...

Gathering, once again


As we already mentioned, we are changing (again) the way resources will be gathered. This of course required yet-another-refactor of that part of code. Last time I was doing this, it was rather boring task: refactor script of one facility, start server, login, spawn some of those facilities, spawn myself needed item, use it, check. Rinse and repeat (sometimes you may hot-reload the script, but you need to still test it manually).

This time I decided to write set of test cases for every 'use event' that results in resource being acquired. And then, those tests will check the stuff for me, so that I do not even need to fire up the client. Nifty!

class Plant : IFacility
{
Item@ item;
uint16 resource;
uint batch;

Plant(Item& item, uin16 resource, uint batch)
{
item.SetEvent(ITEM_EVENT_USE_ON_ME, "_UseItem");
item.SetEvent(ITEM_EVENT_SKILL, "_UseSkill");
item.SetEvent(ITEM_EVENT_FINISH, "_Finish");

@this.item = item;
this.batch = batch;
this.resource = resource;
this.batch = batch;
}

int get_Amount() { return item.Val1; }
void set_Amount(int val) { item.Val1 = val; }

bool UseItem(Critter& cr, Item& usedItem)
{
uint pid = usedItem.GetProtoId();
if(pid == PID_KNIFE || pid == PID_COMBAT_KNIFE || pid == PID_LIL_JESUS_WEAPON || pid == PID_THROWING_KNIFE)
{
if(IsOverweighted(cr))
{
cr.SayMsg(SAY_NETMSG, TEXTMSG_GAME, STR_OVERWEIGHT);
return true;
}
else
{
cr.AddItem(resource, batch);
cr.SayMsg(SAY_NETMSG, TEXTMSG_TEXT, text);
}
return true;
}
else return false;
}
}


This is an excerpt from a class that represents plant facility that we may cut with knife to use the needed resources. It's represented in-game as an item, and we are initializing it in item script in following way:
void item_init(Item& item, bool firstTime)
{
AddFacility(item, Plant(item, PID_FIBER, 2));
}

It uses the pattern I've described in following post. The Plant constructor assigns the events to the item:
// critter uses skill on facility
bool _UseSkill(Item& item, Critter& cr, int skill)
{
IFacility@ facility = GetFacility(item);
if(valid(facility)) return facility.UseSkill(cr, skill);
return false;
}
// critter uses item on facility
bool _UseItem(Item& item, Critter& cr, Item@ usedItem)
{
if(valid(usedItem))
{
IFacility@ facility = GetFacility(item);
if(valid(facility)) return facility.UseItem(cr, usedItem);
}
return false;
}
// :>
void _Finish(Item& item, bool deleted)
{
IFacility@ facility = GetFacility(item);
if(valid(facility)) RemoveFacility(item);
}


Testing stuff


First thing we would probably like to test is, whether critter really receives the items if using proper tool on that facility.Let's first prepare a helper that's gonna greatly reduce the amount of repeated code:
void mock_CritterAddItem(Critter& cr, uint16 pid, uint count)
{
CallExpectation("CritterAddItem_" + pid + "_" + count);
}

class Fixture
{
Critter@ cr;
Item@ tool;
Item@ item;

Fixture(uint16 tool, string@ script)
{
@cr = MockCritter(1);
Mock("Critter::AddItem", "mock_CritterAddItem");
@this.tool = MockItem(tool);
@this.item = MockItem(1); // this pid does not matter
item.SetScript(script);
}

IFacility@ get_Facility() { return GetFacility(item); }
}

And now, the first test:
void test_PlantFruitGivesProperResource()
{
Fixture fix(PID_KNIFE, "prod_plant_fiber@item_init");
fix.Facility.Amount = 100;
ExpectOnce("CritterAddItem_" + PID_FIBER + "_" + 2);

fix.item.EventUseOnMe(fix.cr, fix.tool);

VerifyExpectations();
}

The most important part is the mock for Critter::AddItem, which just stores the expectation named in following manner: CritterAddItem_PID_COUNT. In above test function, we're preparing a context consisting of:
  • critter
  • knife
  • item representing the plant
We invoke the event that would be normally invoked when player or npc would use a knife on the plant (EventUseOnMe). The call process normal behavior, but when it encounters call to Critter:AddItem it calls our mock instead. So we expect that our function is gonna call this function with PID_FIBER and 2 as parameters. Looking into the code of Plant::UseItem, we see that indeed it should work. Plant::batch is equal to 2, so it's gonna execute exactly what we wanted. Running the test confirms that. Yay!

Code is already bugged


Oh great:( I'm writing some code snippets that already contain bugs. So what? Let's fix them, but first let's write a test that confirms them, check if it fails, and then fix the bug and re-check the test:
void test_PlantFruitEmpty()
{
Fixture fix(PID_KNIFE, "prod_plant_fiber@item_init");
fix.Facility.Amount = 0;
ExpectNonce("CritterAddItem_" + PID_FIBER);

fix.item.EventSkill(fix.cr, -1);

VerifyExpectations();
}

Since there are gonna be no timeouts on characters, the facilities itself should somehow limit the amount of resources that we can obtain (and they will be regenerated over time). We've already had implemented some Amount property in our Plant class, but the code didn't care about it. And this test proves that - we expect that AddItem is not gonna be called with PID_FIBER (and whatever count) at all, but it is, because we do not check for amount left in UseItem method. Let's fix it:

            // ...
else
{
if(this.Amount > 0)
{
cr.AddItem(resource, batch);
cr.SayMsg(SAY_NETMSG, TEXTMSG_TEXT, text);
}
}
// ...

And let's run the test again. It succeeds! We've fixed a bug, we tested it. Without doing manual labor.

The method I'm describing here, really makes me happy developer, as I do not need to spend time on manual testing, and also I am preparing more and more tests that's gonna test whether existing features are still working in the future (you know, some change in other area of code can really break the other part of code, in an unexpected way, better catch at least some of such bugs).

There is still one bug left in the code (probably much more, I wrote this post without compiler). I'm leaving it for readers to find out and write test code that proves it!