利用Powershell做简单的单元测试
上周为测试组写了一个用来对测试环境进行配置的小工具,在编写的时候要不停地调试看看功能是否运行正确。我们平时写项目是有unit test framework的,但写个小工具我就难得去加什么正式的unit test了。
不过没unit test,写程序的时候又浑身不舒服,所以就临时想在Powershell里写几个小scrip来做些简单的测试。因为这个工具是用C#写的,所以用Powershell直接调用assembly里的那些函数其实很方便。
在Powershell里要调用一个assembly里的类以及其方法,首先要做的是把这个assembly加载到当前的AppDomain里来。Powershell启动的时候是有一个默认的AppDomain的,其中也已经默认加载了一些.NET的基础assembly,可以用如下命令查看:
然后我写了一个简单的脚本来加载需要测试的assembly:
//load.ps1
if ($args.Length -ne 1 -or $args[0] -eq "/?" -or $args[0] -eq "-?")
{
Write-Host "Usage:"
Write-Host " load.ps1 <Path to .NET assembly>"
return
}
if (!(Test-Path $args[0]))
{
Write-Host "ERROR: Specified .NET assembly does not exist"
return
}
$assemblyPath = resolve-path $args[0]
Write-Host "$assemblyPath"
[System.Reflection.Assembly]::LoadFile($assemblyPath)
这样调用:
PS C:\me\Projects\Powershell> .\load.ps1 MathLib.dll
之后你可以再运行一下:[AppDomain]::CurrentDomain.GetAssemblies()来看看结果如何。
Assembly加载进来后就可以调用其中的类型以及函数了,比如你有如下的C#类:
namespace MathLib
{
public class Math
{
public static double Add(double n1, double n2)
{
return n1 + n2;
}
public static double Subtract(double n1, double n2)
{
return n1 - n2;
}
public double Multiply(double n1, double n2)
{
return n1 * n2;
}
public double Divide(double n1, double n2)
{
return n1 / n2;
}
}
}
你就可以在Powershell下直接这样调用:
PS C:\me\Projects\Powershell> [MathLib.Math]::Add(1, 2)
如果需要调用的不是static的成员函数也很简单,只要用“New-Object”先生成一个对象就可以调用了:
PS C:\me\Projects\Powershell> $obj = New-Object MathLib.Math
PS C:\me\Projects\Powershell> $obj.Multiply(3, 8)
下面这个小脚本可以快速看一下某个类所提供的成员函数:
//list-methods.ps1
$obj = New-Object $args[0]
$methods = $obj.GetType().GetMethods()
foreach ($m in $methods)
{
Write-Host " - " $m.ReturnParameter.Member
}
这之后我其实发现这个方法有一个很大的问题,那就是加载到Powershell默认AppDomain中的assembly是无法动态卸载的,唯一的方法是关闭当前的Powershell窗口。这样的话,如果我们对代码进行了一些改动,但重新编译后却无法覆盖先前的assembly(因为它还是处于被加载的状态,想copy的话系统会报Access Denied)!这确实是很不方便(每次重新编译后,都要关掉Powershell窗口,copy新的assembly,然后再打开一个Powershell)。为了解决这个问题我看了很多资料,试用了N多的方法,不过最终没有找到一个好的解答。
首先要明确的是.NET中没有提供动态卸载单个Assembly的函数(动态加载是可以的,如上文所示)。所以只有通过卸载Assembly所在的那整个AppDomain才可以。不过我们显然不可能卸载Powershell自己所在的当前默认AppDomain,那么自然的答案就是先建立一个新的AppDomain,然后在新的AppDomain里加载所需的Assembly,使用完毕后再卸载整个AppDomain。
这个思路似乎是可行的,但在实际试验中我没有发现任何一种行得通的做法,我试过AppDomain::Load, AppDomain::CreateInstance, AppDomain::CreateInstanceAndUnwrap, AppDomain::CreateInstanceFrom, AppDomain::CreateInstanceFromAndUnwrap等各种方法,都没有成功。其中CreateInstance这一系列函数确实可以生成运行时的一个对象,但因为这些方法返回的结果是一个Object,而在没有将Assembly加载进来前,我又没有相关的类型定义可以用来做typecast,所以空有一个对象却无法调用所需的函数(如果你看完这段还能理解的话,我很佩服;不懂的话也没关系,自己动手试一下很快就明白我的意思了)。
我也查到有些资料说关键是要将你的Aseembly编译为强命名的(strong-named)并且将其加入GAC(过后再从GAC里清理出去即可),比如这篇文章:
http://www.eggheadcafe.com/software/aspnet/30766269/how-to-load--unload-dll.aspx
但我过后发现实际情况并非如此。事实上,你不用把你的assembly加入GAC(strong-named只是允许将assembly加入GAC前的先决条件)也有办法将它加载到一个新的AppDomain中来,下面是我写的代码:
// load-in-new-appdomain.ps1
$appDomain = [System.AppDomain]::CreateDomain("TempDomain")
Write-Host "Assemblies in the temp AppDomain"
$appDomain.GetAssemblies()
Write-Host "Assemblies in the default AppDomain"
[AppDomain]::CurrentDomain.GetAssemblies()
$appDomain.GetAssemblies()[0].GetType()::LoadFile("C:\me\Projects\Powershell\MathLib.dll")
Write-Host "Assemblies in the temp AppDomain after loading"
$appDomain.GetAssemblies()
Write-Host "Assemblies in the default AppDomain after loading"
[AppDomain]::CurrentDomain.GetAssemblies()
[AppDomain]::Unload($appDomain)
Write-Host "Assemblies in the temp AppDomain after unloading"
$appDomain.GetAssemblies()
Write-Host "Assemblies in the default AppDomain after unloading"
[AppDomain]::CurrentDomain.GetAssemblies()
看上去,我确实是对新建的$appDomain在调用LoadFile,但结果呢:
PS C:\me\Projects\Powershell> .\load-in-new-appdomain.ps1
Assemblies in the temp AppDomain
GAC Version Location
--- ------- --------
True v2.0.50727 C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll
...
Assemblies in the temp AppDomain after loading
True v2.0.50727 C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll
Assemblies in the default AppDomain after loading
True v2.0.50727 C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll
True v2.0.50727 C:\Windows\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\1.0.0.0__31bf3856ad364e35\Micros...
...
True v2.0.50727 C:\Windows\assembly\GAC_MSIL\System.Xml\2.0.0.0__b77a5c561934e089\System.Xml.dll
False v2.0.50727 C:\me\Projects\Powershell\MathLib.dll
Assemblies in the temp AppDomain after unloading
Exception calling "GetAssemblies" with "0" argument(s): "Attempted to access an unloaded AppDomain."
At C:\me\Projects\Powershell\load-in-new-appdomain.ps1:18 char:25
+ $appDomain.GetAssemblies( <<<< )
Assemblies in the default AppDomain after unloading
True v2.0.50727 C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll
True v2.0.50727 C:\Windows\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\1.0.0.0__31bf3856ad364e35\Micros...
...
True v2.0.50727 C:\Windows\assembly\GAC_MSIL\System.Xml\2.0.0.0__b77a5c561934e089\System.Xml.dll
False v2.0.50727 C:\me\Projects\Powershell\MathLib.dll
我们的Assembly还是被加载在了默认的AppDomain里!所以我们又回到了square 1,就是我们是无法卸载默认的这个AppDomain的,除非我们关掉Powershell的窗口。
所以最终我没能很好地解决每次重新编译都要关掉Powershell才能copy新的assembly的问题。如果你找到了一个很好的解决方法的话,请不吝赐教。
=========================================================================================================================
后记:写完这篇文章后,我才偶尔看到以下的一篇blog,原来说的是和我同样的意思,呵呵:
Why AppDomains are not a Magic Bullet
posted on 2009-10-09 17:40:21 by demonfox 评论(4) 阅读(6063)