我在Visual Studio 2015 UPD3上使用.NET 4.6.2继承了这个C#测试项目(但它最初是使用以前版本的.NET和VS创建的,可能是VS2012)。
某些测试必须在不同的 AppDomain 中执行,因为必须卸载SUT的DLL(AFAIK,AppDomains是在.NET中卸载程序集的唯一方法)。
测试基础架构使用Console.SetOut
,因此即使从不同的AppDomain,VSTest.Console.exe也会收集对Console.WriteLine的调用,并且所有写入的行都将出现在输出TRX中(通常,调用Console来自不同AppDomains的.WriteLine没有出现在测试输出中。起初我认为这是一个错误,但可能是设计上只有从Microsoft创建的&#34; UnitTestAdapter <调用Console.WriteLine / strong>&#34; appdomain被重新路由到测试输出。)
我试图尽可能地简化它。像往常一样,名字已经改变,以保护无辜和有罪。
////////////////////////AssemblyInitializer class:
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace DomTest
{
[TestClass]
public static class AssemblyInitializer
{
public const int DelayToSimulateWork = 6 * 60 * 1000;
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
StaticDomainManager.DestroyAppDomain();
}
}
}
////////////////////////AppDomainHelper class:
using System;
using System.IO;
namespace DomTest
{
public class AppDomainHelper : IDisposable
{
private AppDomain _domain;
public AppDomainHelper(string domainPrefix)
{
_domain = AppDomain.CreateDomain(
domainPrefix + "_" + Guid.NewGuid(),
null,
AppDomain.CurrentDomain.SetupInformation);
SetConsoleOut();
}
public T CreateInstance<T>() where T : MarshalByRefObject
{
Type type = typeof(T);
return (T)_domain.CreateInstanceAndUnwrap(
type.Assembly.FullName, type.FullName);
}
public void Dispose()
{
Console.WriteLine("AppDomainHelper.Dispose: Calling AppDomain.Unload for AppDomain: {0} [caller domain: {1}]",
_domain.FriendlyName, AppDomain.CurrentDomain.FriendlyName);
AppDomain.Unload(_domain);
_domain = null;
}
public void SetConsoleOut()
{
var consoleOutSetter = CreateInstance<ConsoleOutSetter>();
consoleOutSetter.Set(Console.Out);
}
private class ConsoleOutSetter : MarshalByRefObject
{
public void Set(TextWriter consoleOut)
{
Console.SetOut(consoleOut);
}
public override object InitializeLifetimeService()
{
return null;
}
}
}
}
////////////////////////StaticDomainManager class:
using System;
namespace DomTest
{
public class StaticDomainManager
{
private static AppDomainHelper _currentAppDomain;
public static void DestroyAppDomain()
{
if (_currentAppDomain != null)
{
_currentAppDomain.SetConsoleOut();
_currentAppDomain.Dispose();
_currentAppDomain = null;
}
}
public static void CreateNewAppDomain()
{
DestroyAppDomain();
_currentAppDomain = new AppDomainHelper("SomeName");
}
public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
{
_currentAppDomain.SetConsoleOut();
using (T instance = _currentAppDomain.CreateInstance<T>())
{
action(instance);
}
}
}
}
////////////////////////TestExecutorInAppDomainBase class:
using System;
namespace DomTest
{
public class TestExecutorInAppDomainBase : MarshalByRefObject, IDisposable
{
public TestExecutorInAppDomainBase()
{
Console.WriteLine("TestExecutorInAppDomainBase: Calling some common INIT code in AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
// here: application-specific initialization that must be run in separate AppDomain
}
public void Dispose()
{
// The following call to Console.WriteLine throws RemotingException if a test
// takes too long.
Console.WriteLine("TestExecutorInAppDomainBase: Calling some common UNINIT code in AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
// here: application-specific uninitialization that must be run in separate AppDomain
}
public override object InitializeLifetimeService()
{
return null;
}
}
}
////////////////////////Test1 class (and imagine other classes like this):
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
namespace DomTest
{
[TestClass]
public class Test1
{
[ClassInitialize]
public static void ClassInitialize(TestContext testContext)
{
StaticDomainManager.CreateNewAppDomain();
}
[TestMethod]
public void TestMethod1A()
{
RunTestInDomain((ltc) => ltc.TestAInAppDomain());
}
[TestMethod]
public void TestMethod1B()
{
RunTestInDomain((ltc) => ltc.TestBInAppDomain());
}
private void RunTestInDomain(Action<LocalTestCode> action)
{
StaticDomainManager.ExecuteInDomain<LocalTestCode>(action);
}
private class LocalTestCode : TestExecutorInAppDomainBase
{
public LocalTestCode()
: base()
{
// class-specific code here
}
public void TestAInAppDomain()
{
// class-specific code here
Thread.Sleep(AssemblyInitializer.DelayToSimulateWork);
}
public void TestBInAppDomain()
{
// class-specific code here
Thread.Sleep(AssemblyInitializer.DelayToSimulateWork);
}
}
}
}
所有MarshalByRefObject
派生类重写 InitializeLifetimeService 以返回null
,以避免对象断开连接和RemotingException。
查看相关问题(及其链接):
"Object has been disconnected or does not exist at the server" exception
AppDomain and MarshalByRefObject life time : how to avoid RemotingException?
无论如何,有时候测试会因RemotingException 而失败,因为它们花费的时间太长(代码中的Sleep调用会模拟花费的时间)。
RemotingException发生在TestExecutorInAppDomainBase.Dispose中的Console.WriteLine调用中。
System.Runtime.Remoting.RemotingException: Object '/0d0a9080_ccb7_4db6_a605_6b57778cfb5b/gvcpwryuykjjcowto_nvpdl8_23.rem' has been disconnected or does not exist at the server
我认为问题是Console.Out
的生命周期不受我的控制。 SyncTextWriter
和TextWriter
是MarshalByRefObject
- 派生类,不会覆盖InitializeLifetimeService
(或者说DotPeek说)。因此,如果测试时间过长,则会Console.Out
断开连接并发生RemotingException
。
我尝试用Console.WriteLine
或Debug.WriteLine
替换Trace.WriteLine
(在TestExecutorInAppDomainBase.Dispose中):现在测试不会失败,但这些行不会出现在输出TRX 。如果在不同的AppDomain中调用它们,VSTest.Console似乎不会收集Debug.WriteLine
和Trace.WriteLine
的输出。
同样,这可能是设计中只有在&#34; UnitTestAdapter &#34;内生成的输出。 appdomain被重新路由到测试输出。
所以我决定改变这段代码:
public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
{
_currentAppDomain.SetConsoleOut();
using (T instance = _currentAppDomain.CreateInstance<T>())
{
action(instance);
}
}
到此:
public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
{
_currentAppDomain.SetConsoleOut();
using (T instance = _currentAppDomain.CreateInstance<T>())
{
action(instance);
_currentAppDomain.SetConsoleOut();
}
}
我刚刚在using
块结束之前添加了对SetConsoleOut的额外调用。
通过这种方式,Console.Out
已更新&#34;在调用TestExecutorInAppDomainBase.Dispose之前,并且不会发生RemotingException。
但是我不喜欢这个&#34;解决方案&#34;因为它根本不是解决方案,它是一个快速的黑客攻击,只适用于代码的特定点,而其余的测试项目是一个发条炸弹,只是等待繁荣另一点。
是否有一个很好的解决方案允许:
(thrid bullet是因为我考虑将自定义记录器传递给AppDomain,MarshalByRefObject派生并在我的控制下,但它将涉及更改所有目前调用的测试{{ 1}},它们是 很多 )。