This post is mostly C#-specific, although Java guys can make some use of the ideas presented here.
Simplest attributes
Sometimes, especially when dealing with third-party frameworks such as WCF, you scratch your head for how far can you really go with unit tests to specify that "method X should be a remote operation?". As we all know, firing up a WCF server just to make a call inside unit test is unacceptable. What do we have left?
Let's see, remote operations are marked with OperationContract attribute:
[ServiceContract(Namespace="X.Y.Z")] public interface RemoteEntity { [OperationContract] void DoSomething(); }
So, this is what really defines a remote operation: an attribute. So why not test-drive it? You may think this is insane, until you realize that attributes are just classes that are instantiated and configured at runtime. So, if you test-drive assigning values to C# properties, why not attributes?
Ok, I assume that you calmed down after the last paragraph and don't consider this idea crazy (because it's not). So, the question changes to "how do we specify that method X has attribute Y?". It appears that nUnit has the capability to do this, but it's quite limited.
I've decided to design my own helper, also as an exercise in coding. Using reflection, I came up with the following API for asserting the attribute:
[Test] public void ShouldContainRemoteOperationForDoingSomething() { Assert.IsTrue( Method.Of<RemoteEntity>(entity => entity.DoSomething()) .HasAttribute<OperationContractAttribute>() ); }
And here's the implementation:
public class Method { public static Method Of<T>(Expression<Action<T>> expression) { return new Method((expression.Body as MethodCallExpression).Method); } public bool HasAttribute<T>() { return Attribute.IsDefined(methodInfo, typeof(T)); } private Method(MethodInfo method) { methodInfo = method; } private MethodInfo methodInfo; }
Note that the method Of() takes a lambda expression consisting solely of a call to the method we want to check. Also note that this lambda expression is actually never called! We need it only to extract the metadata for the method that the expression is referring to. Also note that the expression must consist of a single call - otherwise we won't be able to cast it to MethodCallExpression.
Ok, so what if our DoSomething() method takes a parameter? We have to provide it, but the value doesn't really matter, since (remember!) the call is never made and method metadata is extracted from the expression by signature, not by call parameters. So, assuming DoSomething() takes an integer, we can write:
[Test] public void ShouldContainRemoteOperationForDoingSomething() { Assert.IsTrue( Method.Of<RemoteEntity>(entity => entity.DoSomething(Any.Integer())) .HasAttribute<OperationContractAttribute>() ); }
Easy, right? Ok, but what if we have an attribute that takes a constructor parameter and some properties?
Attributes with properties and constructor parameters
Consider this:
public class Book { [StringLength(256, ErrorMessage = "Max 256 characters!")] public string GetDescription(); }
Here, we have to use more elaborate techniques, but we'll try to keep the syntax clean:
[Test] public void ShouldContainLengthValidationFor256Characters() { Assert.IsTrue( Method.Of<Book>(book => book.GetDescription()) .HasAttribute(new StringLengthAttribute(256) { ErrorMessage = "Max 256 characters!" }) ); }
All we have to do is to create an overload for HasAttribute(), taking one parameter instead of none. Note that, compared to the previous examples, in this one, we add the actual instance of the attribute object we are comparing against. This leads to one complication - while the comparison of types was relatively easy to achieve through the Attribute.isDefined() method, comparing objects requires equality to be properly implemented for those objects.
For now, int the following implementation, we'll leave the equality issue for later and close it inside a method named AreEqual(), then I'll show you two ways to go forward with the equality check.
public bool HasAttribute<T>(T expectedAttribute) where T : class { var attrs = Attribute.GetCustomAttributes(methodInfo, typeof(T)); var any = attrs.Any( currentAttribute => AreEqual(expectedAttribute, currentAttribute) ); return any; }
Now for the two methods of filling in this AreEqual() gap. The first option is quite straightforward:
private static bool AreEqual<T_Attribute> ( T_Attribute attr1, Attribute attr2 ) where T_Attribute : class { return attr1.Equals(attr2); }
The implementation above requires the attribute classes to have properly defined equality, which may turn out not to be true in some third party libraries you may be forced to use in your project.
Another option is to use Ploeh's Semantic Comparison library, which is a part of Autofixture. It contains a Likeness class that we can use to write the following:
private static bool AreEqual<T_Attribute> ( T_Attribute attr1, Attribute attr2 ) where T_Attribute : class { return attr2 is T_Attribute && new Likeness<T_Attribute, T_Attribute> (attr2 as T_Attribute).Equals(attr1); }
Likeness works by searching both objects and comparing their public fields and properties (By the way, it's interesting in that it can compare unrelated types, but that's beyond the scope of this post).
Why is it reasonable to compare two attributes by their "likeness"? It's because attributes are mostly used as containers for the information. So, any information you put into the attribute by setting its properties and passing parameters to a constructor, you sooner or later want to get back from it and that's usually achieved by properties.
What about class attributes?
So, what about classes and their attributes? Luckily, nUnit has a decent support for these - just look it up in the documentation! Also, if you dig deep enough into the mentioned topic on nunit discussion list, you'll reach a way to achieve the same end result as this article shows by using extension methods to plug into nUnit's objects used for fluent assertions. It's a good idea to use what I've shown you as a building block for that if you decide to perform this exercise.
Good night everyone and sweet dreams!
No comments:
Post a Comment