27 May
2009

Einfacheres Mocken von Eigenschaften eines Objektes

 

Gestern twitterte Thomas über ein Problem beim Mocken, nach der Lösung blogte er darüber.

Das Problem ist dass Rhino Mocks bei object.AssertWasCalled() bei der Parameter-Überprüfung auf die Gleichheit eines Objektes geprüft wird und somit wenn man nur auf eine bestimmte Property prüfen will dies nicht geht.

Somit schlug dieser Test fehl.

[TestMethod]
public void ChangeEmail_POST_sendet_eine_Aktivierungs_Email_an_den_Benutzer()
{
	// Snipp (Nicht vorhandene Logik des Tests nicht beachten :))
	notificationService.AssertWasCalled(
	n => n.Send(configurationService.GetValue("Email.Sender"), 
	new EmailRecipient { To = "abc@efg.hij" });
}

Für die Lösung musste er .Equals() überschreiben, nur für den Test. Desweiteren gibt es auch Probleme wenn man nur einzelne Properties eines Objektes überprüfen will. Gerade bei komplexeren Klassen kommt dies öfters vor.

Hier eine Lösung wo man .Equals() nicht überschreiben muss, und auch auf einzelne Properties überprüfen kann.

Das Beispiel von Thomas würde damit so aussehen.

[TestMethod]
public void ChangeEmail_POST_sendet_eine_Aktivierungs_Email_an_den_Benutzer()
{
	// Snipp (Nicht vorhandene Logik des Tests nicht beachten :))
	notificationService.AssertWasCalled(
	n => n.Send(configurationService.GetValue("Email.Sender"), 
	Args<EmailRecipient>.MatchProperties( new { To = "abc@efg.hij" });
}

Hiermit wird nur das To-Property des Objekte welches als Parameter übergeben wird überprüft und alle anderen Properties werden ignoriert (sofern man ein anonymes Objekt übergibt). Natürlich kann man auch ein Objekte eines bekannten Typs übergeben, dann müssen jedoch auch alle Properties angegeben werden.

Hier nun der Code dazu.

using System;

using Rhino.Mocks;
using Rhino.Mocks.Constraints;

namespace DerAlbert.UnitTest.Constraints
{
    public static class Anonymous
    {
        public static AbstractConstraint MatchProperties(object expected)
        {
            return new AnonymousPropertyConstraint(expected);
        }
    }

    public static class Args<T> where T : class
    {
        public static T MatchProperties(object expected)
        {
            return Arg<T>.Matches(Anonymous.MatchProperties(expected));
        }
    }
}

und

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

using Rhino.Mocks.Constraints;

namespace DerAlbert.UnitTest.Constraints
{
    public sealed class AnonymousPropertyConstraint : AbstractConstraint
    {
        private readonly object expected;
        private readonly IList<string> messages = new List<string>();

        public AnonymousPropertyConstraint(object expected)
        {
            if (expected == null)
            {
                throw new ArgumentNullException("expected");
            }
            this.expected = expected;
        }

        public override bool Eval(object current)
        {
            return EvalExpectations(current);
        }

        private bool EvalExpectations(object current)
        {
            var expectedPropertyInfos = expected.GetType().GetProperties();

            foreach (var expectedInfo in expectedPropertyInfos)
            {
                object currentValue;
                object expectedValue = GetExpectedValue(expectedInfo);

                if (TryGetCurrentValue(expectedInfo, current, out currentValue))
                {
                    if (!Equals(expectedValue, currentValue))
                    {
                        messages.Add(string.Format(@"'expected value on [{0}.{1}] should be [{2}] but was [{3}]'",
                                                   current.GetType().Name,
                                                   expectedInfo.Name,
                                                   expectedValue,
                                                   currentValue));
                    }
                }
            }
            return messages.Count == 0;
        }

        private object GetExpectedValue(PropertyInfo expectedInfo)
        {
            return expectedInfo.GetValue(expected, new object[0]);
        }

        private bool TryGetCurrentValue(PropertyInfo expectedInfo, object current, out object value)
        {
            value = null;
            var currentInfo = current.GetType().GetProperty(expectedInfo.Name);
            if (currentInfo == null)
            {
                messages.Add(string.Format(@"'expected property [{0}.{1}] does not exists'", current.GetType().Name, expectedInfo.Name));
                return false;
            }
            value = currentInfo.GetValue(current, new object[0]);
            return true;
        }

        public override string Message
        {
            get
            {
                var sb = new StringBuilder();
                foreach (var message in messages)
                {
                    sb.AppendLine(message);
                }
                return sb.ToString();
            }
        }
    }
}

Viel Spaß bei der Anwendung.

Technorati-Tags: ,,,

Der Eintrag ist mir etwas Wert
 

Feedback

# re: Einfacheres Mocken von Eigenschaften eines Objektes

left by Thomas at 5/27/2009 10:17 AM Gravatar
"Viel Spaß bei der Anwendung."

Danke, werde ich haben. Auch wenn ein leichtes Grummeln in der Magengegend bleibt, denn ich bezweifle dass ein Dritter oder man selbst in 2 Jahren noch genau weiß, was da abgeht, eigentlich sollte so ein Aufwand für's Testing nicht nötig sein.

Aber immer noch besser als den tatsächlichen Code nur für Tests anzupassen.

Gruß, Thomas

# re: Einfacheres Mocken von Eigenschaften eines Objektes

left by Albert Weinert at 5/27/2009 10:23 AM Gravatar
Argument Constraints sind ein normaler Mechanismus in Rhino Mocks.

ayende.com/.../Rhino+Mocks+3.5.ashx
Comments have been closed on this topic.