假调用命名空间来测试反射

Fake calling Namespace to test reflection

我有一段代码可以从调用程序集中获取命名空间的特定部分。现在我想对这段代码进行单元测试。 有没有一种方法可以使用 NUnit 伪造调用命名空间的名称,而无需在该特定命名空间中实现 NUnit 测试用例?

这是我要测试的方法:

public static string FindCallingNameSpace()
{
   var stackTrace = new StackTrace();
   var stackFrames = stackTrace.GetFrames();
   if (stackFrames != null)
   {
       int nrFrames = stackFrames.Length;
       for (int i = 0; i < nrFrames; i++)
       {
           var methodBase = stackTrace.GetFrame(i).GetMethod();
           var Class = methodBase.ReflectedType;
           if (Class != null && Class.Namespace != null && Class.Namespace != "Foo.Common.WebService")
           {
               var Namespace = Class.Namespace.Split('.'); 
               return Namespace[1];
           }
       }
    }
    throw new Exception("Can't determine calling namespace! Need this to determine correct api url to call!");
}

例如: Bar.ExampleNs.SomeMethod() 调用 Foo.Common.WebService.CallApi(),它本身调用上面的方法从 SomeMethod() 检索命名空间。结果将是 "ExampleNs".

现在可以创建一个在命名空间 MyUnitTests.ApiTest.TestNameSpace() 中编码但在 Foo.Common.WebService 中的 NUnit 单元测试,调用似乎来自 Bar.ExampleNs.SomeMethod() 因此我可以测试 "ExampleNs"?

我认为到目前为止,实现您所追求的目标的最简单方法是创建一个呼叫转发器并通过该转发器调用 FindCallingNamespace 方法。所以,假设 FindCallingNamespace 方法在 class CallerStuff 中,你创建这个:

namespace SomeNameSpace.ToTest {
    static class RemoteCaller {
        static public string Run() {
            return CallerStuff.FindCallingNameSpace();
        }
    }
}

然后在您的测试中调用 RemoteCaller.Run,而不是 CallerStuff.FindCallingNamespace

但是,您提到了参数化测试,所以大概您最终可能会得到一些您想要测试的不同命名空间,这意味着不同命名空间中有更多的远程调用者,这让我想到可能有一个更通用的方法。

下面的代码实际上通过即时编译然后调用它们来为您创建这些包装器 classes。

class CodeMaker {
    static string _codesectionOne = @"
        using Foo.Common.WebService;
        namespace ";
    static string _codesectionTwo = @" {
            class RemoteCaller {
                static public string Run() {
                    return CallerStuff.FindCallingNameSpace();
                }
            }
        }";

    public static string CompileAndCall(string targetNamespace, 
                                        string referenceAssembly) {
        CompilerParameters CompilerParams = new CompilerParameters();
        string outputDirectory = Directory.GetCurrentDirectory();

        CompilerParams.GenerateInMemory = true;
        CompilerParams.TreatWarningsAsErrors = false;
        CompilerParams.GenerateExecutable = false;
        CompilerParams.CompilerOptions = "/optimize";

        string[] references = { "System.dll", referenceAssembly};
        CompilerParams.ReferencedAssemblies.AddRange(references);

        CSharpCodeProvider provider = new CSharpCodeProvider();
        var codeToCompile = _codesectionOne + targetNamespace + _codesectionTwo;

        CompilerResults compile = provider.CompileAssemblyFromSource(CompilerParams, 
                                                                     codeToCompile);

        if (compile.Errors.HasErrors) {
            string text = "Compile error: ";
            foreach (CompilerError ce in compile.Errors) {
                text += "rn" + ce.ToString();
            }
            throw new Exception(text);
        }

        Module module = compile.CompiledAssembly.GetModules()[0];
        Type mt = null;
        MethodInfo methInfo = null;

        if (module != null) {
            mt = module.GetType(targetNamespace + ".RemoteCaller");
        }

        if (mt != null) {
            methInfo = mt.GetMethod("Run");
        }

        if (methInfo != null) {
            return (string)methInfo.Invoke(null, null);
        }
        throw new InvalidOperationException("It's all gone wrong!");
    }
}

然后您将从测试中调用该方法:

Assert.AreEqual("Fiddle", CodeMaker.CompileAndCall("Wibble.Fiddle.Con", "SO.dll"));
Assert.AreEqual("Fuddle", CodeMaker.CompileAndCall("Wibble.Fuddle.Con", "SO.dll"));

请注意,上例中的 "SO.dll" 是包含 CallerStuff.FindCallingNamespace

的程序集的名称

使用编译器生成调用者 classes 可能对您所追求的东西来说有点矫枉过正,如果您决定使用它,您可能必须调整代码中的错误处理。如果您从不同的测试中多次调用生成的 classes,那么缓存它们也可能是值得的,可能是通过字典键控命名空间而不是每次都编译它们。编译 + 调用代码基于 Simeon Pilgrim 的 this blog post