tags:

views:

307

answers:

5

I have an extension method for testing so I can do this:

var steve = new Zombie();
steve.Mood.ShouldBe("I'm hungry for brains!");

The extension method:

public static void ShouldBe<T>(this T actual, T expected)
{
    Assert.That(actual, Is.EqualTo(expected));
}

This shows:

Expected: "I'm hungry for brains!"
But was:  "I want to shuffle aimlessly"

Is there any hack I can pull off to get the name of the property "BrainsConsumed" from within my extension method? Bonus points would be the instance variable and type Zombie.

UPDATE:

The new ShouldBe:

public static void ShouldBe<T>(this T actual, T expected)
{
    var frame = new StackTrace(true).GetFrame(1);
    var fileName = frame.GetFileName();
    var lineNumber = frame.GetFileLineNumber() - 1;
    var code = File.ReadAllLines(fileName)
        .ElementAt(lineNumber)
        .Trim().TrimEnd(';');

    var codeMessage = new Regex(@"(^.*)(\.\s*ShouldBe\s*\()([^\)]+)\)").Replace(code, @"$1 should be $3");

    var actualMessage = actual.ToString();
    if (actual is string)
        actualMessage = "\"" + actual + "\"";

    var message = string.Format(@"{0} but was {1}", codeMessage, actualMessage);

    Assert.That(actual, Is.EqualTo(expected), message);
}

and this prints out:

steve.Mood should be "I'm hungry for brains!" but was "I want to shuffle aimlessly"

Thanks everyone, esp. Matt Dotson, this is awesome. BTW don't feed the silky trolls people.

A: 

Unfortunately in that position you won't be able to get the name of the property at that stage. The problem is that you're passing the value of the BrainsConsumed field in and there's simply no link back to the Zombie at that point (as far as your method is concerned it's an int and it can't work out where the int originally came from).

The best thing I could come up with for you is that Environment.StackTrace would have the pertinent info in it since you've called steve.BrainsConsumed 1 step up the stack (Only recommend this if what you're trying to do is get some idea what's failed in your unit tests - not if it's to actually traverse up the stack in regular program flow).

Tim Schneider
+1  A: 

No, I don't think you can.

Let's assume that BrainsConsumed is an integer (which looks likely). In that case, the parameter is passed by value - all you get is a copy of the integer you're testing. It has no name apart from the one in the local scope (actual).

This similar question may clarify:

http://stackoverflow.com/questions/72121/finding-the-variable-name-passed-to-a-function-in-c

Damovisa
A: 

This would allow you to test it, but won't get you the name of the method.

You could have this extension:

public static void ShouldBe<T>(this Func<T> func, T expected)
{
    Assert.AreEqual(func(), expected);
}

With the following ussage:

((Func<int>)Program.TestMethod).ShouldBe(2);
Yuriy Faktorovich
+3  A: 

The best I can do would be:

steve.Property(p => p.BrainsConsumed).ShouldBe(0);

or:

steve.ShouldBe(p => p.BrainsConsumed, 0);

or:

Assert.AreEqual(() => steve.BrainsConsumed, 0);

Re:

Bonus points would be the instance variable

By using Expression<Func<TSource, TValue>> (or just Expression<Func<T>>) you can get the property name and value fairly easily. I'll do an example for the middle - note that the first requires an extra type for the DSL, but nothing heavy:

public static class Test
{
    public static void AssertEqual<TSource, TValue>(
        this TSource source,
        Expression<Func<TSource, TValue>> selector,
        TValue expected)
        where TSource : class
    {
        TValue value = selector.Compile()(source);
        string paramName = selector.Parameters[0].Name;

        System.Diagnostics.Debug.Assert(
            EqualityComparer<TValue>.Default.Equals(value, expected),
            typeof(TSource) + " " + paramName + ": " +
                value + " doesn't match expected " + expected);
    }
}

Or slightly better message:

public class Zombie
{
    public int BrainsConsumed { get; set; }
    static void Main() {
        Zombie steve = new Zombie { BrainsConsumed = 2 };
        Test.ShouldBeEqual(() => steve.BrainsConsumed, 0);
    }

}
public static class Test
{
    static string GetName(Expression expr)
    {
        if (expr.NodeType == ExpressionType.MemberAccess)
        {
            var me = (MemberExpression)expr;
            string name = me.Member.Name, subExpr = GetName(me.Expression);
            return string.IsNullOrEmpty(subExpr)
                ? name : (subExpr + "." + name);
        }
        return "";
    }
    public static void ShouldBeEqual<TValue>(
        Expression<Func<TValue>> selector,
        TValue expected)
    {
        TValue value = selector.Compile()();

        string name = GetName(selector.Body);

        System.Diagnostics.Debug.Assert(
            EqualityComparer<TValue>.Default.Equals(value, expected),
            typeof(TValue) + " " + name + ": " +
                value + " doesn't match expected " + expected);
    }
}
Marc Gravell
I don't quite understand why you would post code that is so confusing and hard to follow, and, if I may, is generally bad practice. But to each his own; it just seems slightly irresponsible. As a learning process it's fine, but if anyone actually did this, they should be shot ...
Noon Silk
@silky: Which part of it is hard to follow and how would you make it simpler?
Yuriy Faktorovich
Erm... because that is the most reliable way to get a property name and/or variable name from a code expression?
Marc Gravell
Yuriy: I'm not going to explain what is obvious to someone who has the capability to work it out for themselves.
Noon Silk
@silky And how, pray tell, would you solve this yourself?
hmemcpy
hmemcpy: I wouldn't try and solve it (hence I didn't). It's not an appropriate question; I'm almost certain (though I could be wrong) that there is no legitimate use for this, and the underlying aim can be achieved in a better manner.
Noon Silk
@silky - it's a lot more readable to me as `steve.BrainsEaten.ShouldBe(0)` than `Assert.AreEqual(0, steve.BrainsEaten, "steve.BrainsEaten doesn't match expected: 0")`
Damovisa
@silky The questioner is obviously stretching the C# language, in this case so that his tests read as clearly as possible. Let's suspend that thought "I could be wrong" for just a little longer...I can attest that the final result of this question having a good answer is a test-fail message which takes a line of test code (which is readable in its natural-language syntax) and then produces what I would have thought to be an impossibly detailed error message - a task which I would normally have achieved by writing an extra line of explanation per test, to be printed as the error message.
PandaWood
+3  A: 

You can get the code if it's a debug build by using some of the diagnostics classes. Given that this is for unit tests, DEBUG is probably reasonable.

public static void ShouldBe<T>(this T actual, T expected)

{

var frame = new StackTrace(true).GetFrame(1);
var fileName = frame.GetFileName();
var lineNumber = frame.GetFileLineNumber() - 1;

string code = File.ReadLines(fileName).ElementAt(lineNumber).Trim();

Debug.Assert(actual.Equals(expected), code);

}

For your example, code = "steve.BrainsConsumed.ShouldBe(0);"

Obviously you should add some error checking to this code, and you could probably make it faster by not reading all the lines in the file.

Matt Dotson