17

Is it possible to make this test not throw an exception? It appears that adding any non-GACed class into the logical call context causes an exception to be thrown in line 2 of the test.

Test 'TestProject1.UnitTest1.TestMethod1' failed: Test method TestProject1.UnitTest1.TestMethod1 threw exception: System.Configuration.ConfigurationErrorsException: An error occurred loading a configuration file: Type is not resolved for member 'TestProject1.Bar,TestProject1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. ---> System.Runtime.Serialization.SerializationException: Type is not resolved for member 'TestProject1.Bar,TestProject1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

namespace TestProject1 {
    [ Serializable]
    public class Bar {

    }

    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void TestMethod1() {
            CallContext.LogicalSetData("foo", new Bar());
            ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None );
        }
    }
}

WHY?!?!?

Peter Huene
  • 5,758
  • 2
  • 34
  • 35
aquinas
  • 23,318
  • 5
  • 58
  • 81

4 Answers4

28

Basically, this is a problem with the design of the process used to host the test code during a test run. Tests are run in a separate AppDomain from the test host process' default AppDomain. When a call is made from one AppDomain to another, the call context needs to be deserialized into the target AppDomain. In your case, ConfigurationManager.OpenExeConfiguration ends up calling AppDomain.get_Evidence. This results in a call from the AppDomain hosting the tests to the default AppDomain.

The default AppDomain's base directory is where the test host executable was installed to. By default, this is "%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE". The AppDomain hosting the tests uses a base directory of the test run deployment location. By default, this is "<solutiondir>\TestResults\<testrun>\Out". Thus, the AppDomain used to run the tests can resolve the serialized types as their assemblies lie in the Out directory (regardless, they're already loaded into the test's AppDomain because this code executes) while the default AppDomain cannot.

GACing the assemblies containing the serialized types works because the GAC is probed during assembly resolution. This is one possible solution. However, you would need to install them to the GAC on every test run and this requires strong names and administrative privileges.

Another possible solution would be to copy the assemblies to the probing path of the default AppDomain. This would be the base directory listed above and the privatePaths listed in QTAgent.exe.config or QTAgent32.exe.config (which one runs depends on whether or not you're using a 64-bit operating system and the hosting platform setting in the test settings). As with all private probing paths, these directories must be sub-directories of the base directory. You could create a new sub-directory with the appropriate access permissions, add the directory name to the privatePaths in the .exe.config files, and then copy the assemblies containing the serialized types into this directory during test setup. This solution would not require administrative privileges or strong naming, provided the directory you're copying the assemblies to allows you to write to it.

Unfortunately, neither of these solutions are ideal as they can introduce the possibility for type mismatch between the code running in the tests' AppDomain and the default AppDomain if the assemblies are not updated properly before each test run. A proper fix would be required from Microsoft (say having the test host process automatically probe the test deployment directory when the default AppDomain needs to resolve types). I didn't work on the test tools, so I can't comment on what a real fix, taking implementation details into consideration, would require.

Edit: if you do elect one of these "solutions", you will also need to disable the test host process from staying alive between test runs. This is because the default AppDomain will stick around and the assemblies containing the serialized types will remain loaded, preventing you from updating them on the next test run. The option to control this is "Keep test execution engine running between test runs" under "Tools -> Options -> Test Tools -> Test Execution".

Peter Huene
  • 5,758
  • 2
  • 34
  • 35
  • You can quickly verify the above information by manually copying an up-to-date test assembly from your example to "%ProgramFiles%\Microsoft Visual Studio 2010\Common7\IDE", running your test (it should pass), killing QTAgent[32].exe, and then deleting the test assembly you copied. – Peter Huene Mar 28 '11 at 22:13
  • @aquinas - you seem to know in detail what you're talking about. But is there any way you can describe at least the problem in such a way that us laymen might understand it? I'm reading your explanation slowly and still not getting how calling LogicalSetData on the call context throws the open-exe-configuration call. – joniba Jun 13 '11 at 15:00
  • 1
    You'll end up in a sticky pickle like myself if your attempting to use `.testsettings` setup and cleanup scripts to modify the contents of probing paths referred to by QTAgent. You'll get Access denied because file is in use, and if you try to kill QTAgent to free the files, then you'll also terminate the execution of the setup or cleanup script. Catch 22. Best to `CallContext.FreeNamedDataSlot` if you're having to pursue this kind of approach. – Rabid Mar 21 '13 at 14:45
14

This problem can be solved by having Bar implement MarshalByRefObject. This allows the class to be referenced in the AppDomain that the test runner is running inside.

[ Serializable]
public class Bar : MarshalByRefObject {

}
CodeMonkeyKing
  • 4,556
  • 1
  • 32
  • 34
  • 2
    Had this issue with NUnit running tests that started to fail. This solution helped in that scenario. The object in question was stored in CallContext.LogicalSetData(). – Manfred Jul 05 '18 at 09:19
9

When using logical CallContext to store objects implement the appropriate finalization logic (IDisposable) to clean up objects stored in the CallContext (e.g.: CallContext.FreeNamedDataSlot)

Hope this helps,

Juanjo

jdearana
  • 1,089
  • 12
  • 17
0

Another workaround -that worked for me- could be to initialize the configuration system before any object is added to the logical call context.

In my case, WCF and WebAPI services were self-hosted within the test environment to have high control of their behaviors (e.g. replace components within IoC container), but had this serialization exception when execution reached authentication via Active Directory. With the help of reflection tool (e.g. IL Spy), I could identify the configuration section (system.directoryservices) AD was trying to read and triggering cross domain serialization issue.

Placing the following code before starting self-hosted services, solved the problem:

System.Configuration.ConfigurationManager.GetSection("system.directoryservices");

You can read more explanation here and here

hollo
  • 41
  • 3