7

I'm working through Kent Beck's TDD by Example as an academic exercise, but using MSpec to write the tests. When following worked examples, I like to introduce a twist so that I can't simply copy the text out rote, I find that way I tend to run into problems that I have to solve and as a result, end up learning much more. I believe this is one of those occasions.

I'm part way through Kent's 'money' example. Here's the class structure I have: enter image description here

I have the following two test contexts:

[Subject(typeof(Money), "Equality")]
public class when_comparing_different_classes_for_equality
{
    Because of = () => FiveFrancs = new Franc(5, "CHF");
    It should_equal_money_with_currency_set_to_francs = () => FiveFrancs.Equals(new Money(5, "CHF")).ShouldBeTrue();
    static Franc FiveFrancs;
}

[Subject(typeof(Franc), "multiplication")]
public class when_multiplying_a_franc_amount_by_an_integer
{
    Because of = () => FiveFrancs = new Franc(5, null);
    It should_be_ten_francs_when_multiplied_by_2 = () => FiveFrancs.Times(2).ShouldEqual(Money.Franc(10));
    It should_be_fifteen_francs_when_multiplied_by_3 = () => FiveFrancs.Times(3).ShouldEqual(Money.Franc(15));
    static Franc FiveFrancs;
}

The Times() method returns a new object of type Money containing the result, i.e. the objects are immutable. the first context above passes, suggesting that Equals is working as expected, i.e. it ignores the the object types, so long as they are both inherited from Money, and only compares that the amount and currency fields are equal. The second context fails with output similar to this:

Machine.Specifications.SpecificationException
  Expected: TDDByExample.Money.Specifications.Franc:[15]
  But was:  TDDByExample.Money.Specifications.Money:[15]
   at TDDByExample.Money.Specifications.when_multiplying_a_franc_amount_by_an_integer.<.ctor>b__2() in MoneySpecs.cs: line 29

Equality is defined as the amount (value) and currency being the same; the actual type of the object is supposed to be ignored, so the intended result is that it shouldn't matter if I'm testing equality with Money or Franc objects, as long as the amount and currency fields are the same. However, things are not working as planned. When debugging, my Equals() methods are not even getting called. There is clearly something I'm not understanding here. I am sure the solution will be blindingly obvious when I know it, but I can't see it for looking. Can anyone offer a suggestion as to what I need to do to make this work?

Here's the implementation of Equals():

public bool Equals(Money other)
{
    return amount == other.amount && currency == other.currency;
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj))
        return false;
    if (ReferenceEquals(this, obj))
        return true;
    return Equals(obj as Money);
}
Anthony Mastrean
  • 21,850
  • 21
  • 110
  • 188
Tim Long
  • 13,508
  • 19
  • 79
  • 147
  • After much consideration, I believe this may be an MSpec issue. MSpec compares the object types and failes the comparison based on the types not being the same. Only if the types are equal does MSpec then go on to use a value based comparison. This is why my Equals method is never getting called. However, I think Liskov says that I should be able to compare a Money with a Franc if my definition of equality allows it. Therefore I have opened an issue with the MSpec project. https://github.com/machine/machine.specifications/issues/200 – Tim Long Jan 14 '14 at 17:41
  • @Anthony I prefer Whitesmiths style indentation (braces indented). I don't particularly mind you changing my indentation and I'm happy to let your edits stand, but on the other hand it seems a bit presumptuous to override other people's preferences. Is there a guideline that I'm not aware of? – Tim Long Jan 14 '14 at 17:52
  • Oh, I'm sorry, I did totally just assume you had a bad spaces/tab mix or something. I'm getting all glossy-eyed from the general bad code block formatting of SO. – Anthony Mastrean Jan 14 '14 at 19:26
  • PS. you're probably the only person on Earth using Whitesmith style braces for C#, then again, you're also wearing a bowtie in your profile picture, so you seem a generally unique character :D – Anthony Mastrean Jan 14 '14 at 19:28
  • "Bow ties are cool, you know!" (the 12th doctor). I learned that style of indentation at university back on the 80s and its one of those habits I find really hard to kick. My lecturer in compiler writing claimed that, since braces define a block, then they are part of the block and should be indented with it. He was very clear on that point, we dared not disagree ;-) – Tim Long Jan 14 '14 at 20:39

2 Answers2

2

A fully-complete implementation of equality would look like this. See if it helps.

protected bool Equals(Money other)
{
    // maybe you want this extra param to Equals?
    // StringComparison.InvariantCulture
    return amount == other.amount 
      && string.Equals(currency, other.currency);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    var other = obj as Money;
    return other != null && Equals(other);
}

public override int GetHashCode()
{
    unchecked
    {
        return (amount * 997) ^ currency.GetHashCode();
    }
}

public static bool operator ==(Money left, Money right)
{
    return Equals(left, right);
}

public static bool operator !=(Money left, Money right)
{
    return !Equals(left, right);
}
Anthony Mastrean
  • 21,850
  • 21
  • 110
  • 188
spender
  • 117,338
  • 33
  • 229
  • 351
  • This kind of definition is ONLY correct if the `amount` and `currency` fields are read-only (or guaranteed never to change). It is REQUIRED that the value returned from `GetHashCode` is immutable for the life-time of the instance. – Enigmativity Jan 14 '14 at 02:47
  • @Enigmativity: Indeed, as stated in the question above: "i.e. the objects are immutable" – spender Jan 14 '14 at 02:48
  • Although this is a very thorough and complete implementation of equality, it does not answer the OP question, of why it is not being called on the first place. – Xint0 Jan 14 '14 at 02:49
  • @spender - Just providing clarity. The immutability isn't terribly well advertised and I've seen a lot of people screw up code by making mutable `GetHashCode` implementations. – Enigmativity Jan 14 '14 at 02:57
0

As @Harrison commented, the problem is the result type of the Times method of your Franc class. It fails the test specification because it returns a Money object, but the specification expects a Franc instance. Either change the specification to require a Money object or override the Times method to return a Franc instance.

Update

After updating the test specification, you changed the lines:

It should_be_ten_francs_when_multiplied_by_2 = () => FiveFrancs.Times(2).ShouldEqual(Money.Franc(10));
It should_be_fifteen_francs_when_multiplied_by_3 = () => FiveFrancs.Times(3).ShouldEqual(Money.Franc(15));

But, on the attribute the type of the subject still is:

[Subject(typeof(Franc), "multiplication")]

So I think it is still expecting a Franc instance instead of a Money instance.

Xint0
  • 5,221
  • 2
  • 27
  • 29
  • Apologies, I've updated this question 3 times within the last 10 minutes, I wasn't expecting such a quick response! Does your answer still hold true after my updates? – Tim Long Jan 14 '14 at 02:46
  • The [Subject] attribute is only for documentation, it doesn't have any effect on the code. It may seem odd but the example is mid-refactor and it is moving towards eliminating the Franc and Dollar classes in favour of a single general-purpose Money class. It _is_ something of a contrived situation, therefore. I could simply cut to the chase and put the finished refactored code in there, but I wanted to understand how this has gone wrong. I'm still not seeing why it doesn't work. – Tim Long Jan 14 '14 at 02:49
  • I'm not so sure about the `[Subject]` attribute, but the only difference I can see from the expected is the type of the object. Have you tried changing the `[Subject]` attribute to use the `Money` type? – Xint0 Jan 14 '14 at 02:58
  • I did try changing the attribute, and it had no effect. I am 100% certain that it has no effect other than for documentation (in fact, you can omit it altogether). However, I think I am nearer to the solution. I noticed that the static field was declared as Franc instead of Money. Having changed it to Money, the test still fails but I now have a much clearer indication of what the problem is: Expected: TDDByExample.Money.Specifications.Franc:[15 CHF] But was: TDDByExample.Money.Specifications.Money:[15] – Tim Long Jan 14 '14 at 03:05
  • @TimLong : oops. The Franc constructor should look like this, no? `public Franc(int amount):base(amount,"CHF"){}`? :-) – spender Jan 14 '14 at 03:14
  • @TimLong Great! Not exactly what I thought, but glad to help you hit the nail. – Xint0 Jan 14 '14 at 03:14
  • I'm glad I can stop trawling the mSpec source now! – spender Jan 14 '14 at 03:15
  • @spender not so fast! It was stupid-o-clock when I posted my last comment and I had to give up and get some sleep. Its still not solved. The reason the Franc constructor doesn't look right is because I'm half way through a refactoring example which will culminate in the class being eliminated altogether. Its a snapshot of a half-complete process. However, the point of the exercise is to demonstrate how the refactor can be completed safely within a TDD methodology, while maintaining working code (green bar) at all times. My bar is red, so I need to fix it before I can complete the next step. – Tim Long Jan 14 '14 at 16:54
  • I am starting to think this is an MSpec issue. MSpec uses several different 'comparer strategies' to perform an equality comparison, however it uses `TypeComparer()` _before_ it uses `ComparableComparer()` therefore my test is always doomed to fail. MSpec assumes that different types can never be equal, however my definition of equality allows different types to be equal. I'll raise a bug report with the MSpec guys and see what they think. – Tim Long Jan 14 '14 at 17:06
  • @TimLong I was looking at the fallthroughs for the comparers, wondering if you were hitting any on the way down! – spender Jan 14 '14 at 17:18