C
C#4w ago
kniemiec

Handling Custom Assembly Attributes in Unit Tests After Migrating from .NET Framework 4.8 to .NET 8

I recently migrated my app from .NET Framework 4.8 to .NET 8 and ran into an issue with unit tests in MS Test. The problem is related to the OriginSetupPackage custom attribute applied to the entry assembly. In .NET Framework 4.8, the Product class loads this custom attribute from the entry assembly, like so:
see code sample 1 in attached screenshot
see code sample 1 in attached screenshot
The Product class reads the attribute in its static constructor:
see code sample 2 in attached screenshot
see code sample 2 in attached screenshot
This works fine in the app, but in unit tests, the entry assembly is testhost.dll, which doesn't have the required attribute. This causes the following exception:
see code sample 3 in attached screenshot
see code sample 3 in attached screenshot
In .NET Framework 4.8, we worked around this by manipulating AppDomain via AppDomainManager to set the test assembly as the entry assembly, like this:
see code sample 4 in attached screenshot
see code sample 4 in attached screenshot
This allowed us to set the entry assembly correctly for tests. However, in .NET 8, AppDomainManager is deprecated, and manipulating the entry assembly directly is no longer possible. In MS Test, the entry assembly is now testhost.dll, leading to the same exception. How can this be resolved in .NET 8 without modifying the Product class?
No description
4 Replies
canton7
canton74w ago
Frame challenge: * Do you have to query the entry assembly, or can you query the assembly containing some know type? * Does Product need to query this in its cctor? Can you use dependency injection instead (and mock out Product for unit tests), or if it needs to be static, have a way to override its value for unit tests?
kniemiec
kniemiecOP4w ago
1. It has to be entry assembly. But if it wasnt for it, how would you solve it? 2. Unfortunately, it has to. DI would be ideal but this is comming from some old code, which lets say, nowadays you wouldn't write. So yes, is has to be like this and in static constructor, is there a way to mock static constructor? Any solution or idea would be appreaciated
canton7
canton74w ago
But if it wasnt for it, how would you solve it?
typeof(SomeKnownType).Assembly
is there a way to mock static constructor?
No, but you can pull some shenanigans. For example, a static property on Product which overrides its behaviour of pulling the package from an attribute. Or a define: build your assembly with UNIT_TEST defined, and use that with #if to change the behaviour to not look at attributes. Or anything like that (Note, it would have been really helpful if you had provided your code as actual text that I could copy/paste here to make some examples)
kniemiec
kniemiecOP4w ago
Specyfing
#if UNIT_TEST
#if UNIT_TEST
was also something I thought about, but I was wondering if it is valid solution. Also solution that doesn't involve modyfing Product class code would be the best, since it is not my code. But if it is the only workaround I'll probably need to convice someone to implement it that way Here is code, I had to make it screenshot due to character limitation 1)
[assembly: OriginSetupPackage(SetupPackage.Master)]

namespace App
{
// Some app code...
}
[assembly: OriginSetupPackage(SetupPackage.Master)]

namespace App
{
// Some app code...
}
2)
namespace App.Environment
{
public class Product
{
static Product()
{
// This method call needs to read the OriginSetupPackage attribute from the entry assembly
m_ProductPackageLazy = new Lazy<SetupPackage>(() =>
{
object[] originSetupPackageAttributes =
System.Reflection.Assembly.GetEntryAssembly()
.GetCustomAttributes(typeof(OriginSetupPackageAttribute), false);

if (originSetupPackageAttributes == null ||
originSetupPackageAttributes.Length != 1 ||
!(originSetupPackageAttributes[0] is OriginSetupPackageAttribute))
{
throw new Exception("The program assembly has no OriginSetupPackage attribute.");
}

return ((OriginSetupPackageAttribute)originSetupPackageAttributes[0]).Package;
});
}

private static Lazy<SetupPackage> m_ProductPackageLazy;
}
}
namespace App.Environment
{
public class Product
{
static Product()
{
// This method call needs to read the OriginSetupPackage attribute from the entry assembly
m_ProductPackageLazy = new Lazy<SetupPackage>(() =>
{
object[] originSetupPackageAttributes =
System.Reflection.Assembly.GetEntryAssembly()
.GetCustomAttributes(typeof(OriginSetupPackageAttribute), false);

if (originSetupPackageAttributes == null ||
originSetupPackageAttributes.Length != 1 ||
!(originSetupPackageAttributes[0] is OriginSetupPackageAttribute))
{
throw new Exception("The program assembly has no OriginSetupPackage attribute.");
}

return ((OriginSetupPackageAttribute)originSetupPackageAttributes[0]).Package;
});
}

private static Lazy<SetupPackage> m_ProductPackageLazy;
}
}
3)
if (originSetupPackageAttributes == null ||
originSetupPackageAttributes.Length != 1 ||
!(originSetupPackageAttributes[0] is OriginSetupPackageAttribute))
{
throw new Exception("The program assembly has no OriginSetupPackage attribute.");
}
if (originSetupPackageAttributes == null ||
originSetupPackageAttributes.Length != 1 ||
!(originSetupPackageAttributes[0] is OriginSetupPackageAttribute))
{
throw new Exception("The program assembly has no OriginSetupPackage attribute.");
}
4)
[assembly: OriginSetupPackage(SetupPackage.Master)]

namespace App.Test
{
public class InitTest
{
public InitTest()
{
Assembly assembly = Assembly.GetCallingAssembly();

AppDomainManager manager = new();
FieldInfo entryAssemblyField = manager.GetType().GetField("_entryAssembly", BindingFlags.Instance | BindingFlags.NonPublic);
entryAssemblyField.SetValue(manager, assembly);

AppDomain domain = AppDomain.CurrentDomain;
FieldInfo domainManagerField = domain.GetType().GetField("_domainManager", BindingFlags.Instance | BindingFlags.NonPublic);
domainManagerField.SetValue(domain, manager);

Product.Initialize($"Server={TestConfig.ServerAddress}:{TestConfig.ServerPort}");
}
}
}
[assembly: OriginSetupPackage(SetupPackage.Master)]

namespace App.Test
{
public class InitTest
{
public InitTest()
{
Assembly assembly = Assembly.GetCallingAssembly();

AppDomainManager manager = new();
FieldInfo entryAssemblyField = manager.GetType().GetField("_entryAssembly", BindingFlags.Instance | BindingFlags.NonPublic);
entryAssemblyField.SetValue(manager, assembly);

AppDomain domain = AppDomain.CurrentDomain;
FieldInfo domainManagerField = domain.GetType().GetField("_domainManager", BindingFlags.Instance | BindingFlags.NonPublic);
domainManagerField.SetValue(domain, manager);

Product.Initialize($"Server={TestConfig.ServerAddress}:{TestConfig.ServerPort}");
}
}
}

Did you find this page helpful?