Recently I have rewritten a class I use for accessing non-public fields and properties during unit testing and simplified it a little.
The problem:
Unit tests and testing code is always kept in a separate assembly. To effectively unit test a class you will need to be able to set properties and fields that are not public to give that class under test its dependencies. The first mistake some people make is to make properties public or create more constructors. This is a very smelly idea. It makes the production code more difficult to use as it appears consumers have access to more members on a class than they need. Inevitably, they will use some inappropriate members and it will blow up.
My Previous Solutions:
One solution is to use an assembly based attribute that extends trust from one assembly to another and allows the other assembly access to its internal members. See an earlier blog post for more information. This is still not ideal as it is polluting a production library and still only means you can access a class's internals when you need to access its privates too. (No pun intended).
Tim Stall gives a good overview of some more tried and true techniques here. These however, seem to require polluting actual code with test harnesses etc, not ideal.
I have also seen a extension method based technique to create a utility style class to wrap some of the more complex reflection nastiness.
MSTest also has a real nice feature for auto-generating private accessors, see msdn for more information. I personally don't like mstest, see pros and cons here and here. (Basically, NUnit has better mocking support, easier integration into CI, and runs faster and more predictably with multi-threading).
I have tried using a hybrid of MsTest and NUnit, and although this allows use of MsTest's private accessor generator, it also means both libraries must be referenced. That is a bad idea as it is too easy for a developer to mistakenly use the wrong test attributes and those tests are not run during the CI process.
A "Not-Bad" Solution:
Using a "private accessor" it is possible to reflect across a type and invoke any public member as long as you can spell the name of the member as a string. (I have seen some interesting cases of developer spelling, for example Organator meant to be Originator, Igor, tsk tsk).
My current solution is to use a helper style static class called PrivateAccessor that has easier to use methods than using reflection directly.
Pros / Cons:
+ Enables access to any member or constructor on a type.
- It uses reflection, which always seems to smell funny. (But hey this is test code only).
- If a member is renamed the test will not give a compile time warning. Meaning the offending name changer will probably not fix the test.
+ Test will fail if member is renamed.
- Syntax is a little lengthy and wordy. (But again test code needn't be supermodel status).
Usage:
Consider this class in a production library.
31 public class Class1 {
32 internal Class1(string something) {
33 }
34
35 public void DoSomething() {
36 var existingInstance = new Widget();
37 ObjectFactory.Initialize(init => {
38 init.For<ILogWriter>().Use<FakeLogWriter>();
39 init.For<IService>().Singleton().Use<MyService>();
40 init.For<IWidget>().Use(existingInstance);
41 init.For<IComplex>().Use(() => new ComplexToBuildObject());
42 });
43
44 var service = ObjectFactory.Container.GetInstance<Service.IService>();
45 }
46 }
The class has a non-public accessor, so ordinarily it cannot be created outside its own assembly. We need to create a new instance to test it, without relying on any other peice of code. We only want to target this class for testing.
A test to create and test the constructor.
96 [Test]
97 public void GetObject() {
98 var target = PrivateAccessor.PrivateConstructor<Class1>(() => "something");
99 Assert.IsNotNull(target);
100 }
The Private Accessor wraps the nasty reflection code and makes it easier to use and read. Lambda is used to pass in strongly typed arguments for the constructor.
The private accessor code:
10 public static class PrivateAccessor {
11 public static T PrivateConstructor<T>() where T : class {
12 var type = typeof(T);
13 var constructor = type.GetConstructor(new Type[] { });
14 return constructor.Invoke(new object[] { }) as T;
15 }
16
17 public static TClass PrivateConstructor<TClass>(params Func<object>[] getArguments)where TClass : class {
18 var type = typeof(TClass);
19 var args = (from getter in getArguments
20 select getter())
21 .ToArray();
22 var argTypes = (from arg in args
23 select arg.GetType())
24 .ToArray();
25 var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance |BindingFlags.Public, null, argTypes, null);
26 return ctor.Invoke(args) as TClass;
27 }
28 }
In exactly the same way properties can be "getted" and "setted"
29 public static object GetProperty(object instance, string propertyName) {
30 if (instance == null) {
31 throw new ArgumentNullException("instance");
32 }
33
34 if (string.IsNullOrEmpty(propertyName)) {
35 throw new ArgumentNullException("propertyName");
36 }
37
38 var info = instance.GetType().GetProperty(propertyName, BindingFlags.Instance |BindingFlags.NonPublic | BindingFlags.Public);
39 return info.GetValue(instance, new object[] { });
40 }
41
42 public static void SetProperty(object instance, string propertyName, object value) {
43 if (instance == null) {
44 throw new ArgumentNullException("instance");
45 }
46
47 if (string.IsNullOrEmpty(propertyName)) {
48 throw new ArgumentNullException("propertyName");
49 }
50
51 var info = instance.GetType().GetProperty(propertyName, BindingFlags.Instance |BindingFlags.NonPublic | BindingFlags.Public);
52 info.SetValue(instance, value, new object[] { });
53 }
And of course methods, constants and fields can be access in the same way.
The only thing I don't like about it is having to use magic strings that will not give me compile time errors if things are not right.
For example:
122 [Test]
123 public void GetObject() {
124 var target = PrivateAccessor.PrivateConstructor<Class1>(() => "something");
125 Assert.IsNotNull(target);
126
127 var propertyValue = PrivateAccessor.GetProperty(target, "State") as string;
128 Assert.AreEqual("OK", propertyValue);
129 }
Now if the State property changes its name or is removed, the test will compile, but fortunately will not pass, not really ideal, because the offending developer has already checked in the code with running the tests and has broken the build, discovering the problem a little too late.
In Part 2 I'll attempt to write a T4 Code Generator that will allow strongly typed access to non-public members.
No comments:
Post a Comment