在设计或构建时生成方法(C#)

时间:2011-12-15 16:02:10

标签: c# code-generation aop

我有一个集成测试解决方案。我在XML文件中描述了我的测试。为了利用Visual Studio 2010测试基础架构,我有一个C#类,其中每个XML测试文件都有一个关联方法,用于加载XML文件并执行其内容。它看起来像这样:

[TestClass]
public class SampleTests
{

    [TestMethod]
    public void Test1()
    {
        XamlTestManager.ConductTest();
    }

    [TestMethod]
    public void Test2()
    {
        XamlTestManager.ConductTest();
    }

    ...

    [TestMethod]
    public void TestN()
    {
        XamlTestManager.ConductTest();
    }
}

每个方法名称对应一个XML文件名。因此,我必须在我的测试目录中包含以下文件:

  • Test1.xml
  • Test2.xml
  • ...
  • TestN.xml

XamlTestManager.ConductTest()使用StackTrace类获取调用方法的名称,这样它就可以找到要加载的正确XML测试文件。

每当我更改测试,添加/删除/重命名XML测试文件时,我想摆脱添加/删除/重命名测试方法的额外管理。 如何根据测试目录中的实际XML文件在编译过程中自动生成此类或其方法?

选项1: 我考虑过PostSharp,但它不允许我查找XML文件并动态生成方法(或者我是肤浅的?)。 选项2: 另一个想法是构建一个Visual Studio自定义工具,无论何时执行它都会生成代码。这里的缺点是部署。自定义工具需要注册到VS.我想要一个可以提交到存储库的解决方案,将其签出到另一台计算机并立即使用它。 (我相信简单。“检查并运行”只是简单地简化了新开发人员的生活,如果他们在编译运行应用程序之前不需要经过安装的事情列表。)

您有什么建议,如何摆脱不必要的维护问题?

修改
对于Justin的请求,我添加了更多细节。我们使用Bizunit(太棒了!!!)作为我们框架的基础,使用大量定制的高级测试步骤。从这些步骤开始,我们可以以声明的方式从乐高积木构建我们的测试。我们的步骤包括FileDrop,WebService调用甚至轮询,启动一个完整的Web服务器来模拟合作伙伴Web应用程序,随机数据生成器,数据比较步骤等。这是一个示例测试xml(实际上是XAML):

<TestCase BizUnitVersion="4.0.154.0" Name="StackOverflowSample" xmlns="clr-namespace:BizUnit.Xaml;assembly=BizUnit" xmlns:nib="clr-namespace:MyCompany.IntegrationTest;assembly=BizUnit.MyCustomSteps" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <TestCase.SetupSteps>
    <nib:ClearStep FailOnError="True" RunConcurrently="False" />
    <nib:LaunchSimulatedApp AppKernelCacheKey="provider" FailOnError="True" FireWakeUpCall="False" PortNumber="4000" RepresentedSystem="MyProviderService" RunConcurrently="False" />
    <nib:HttpGetStep FailOnError="True" RunConcurrently="False" Url="http://localhost:10000/Home/StartSvgPolling">
      <nib:HttpGetStep.Parameters>
        <x:String x:Key="PolledAddress">http://localhost:4000/SvgOutputPort.asmx</x:String>
        <x:String x:Key="PollingInterval">10</x:String>
        <x:String x:Key="FilterFile"></x:String>
      </nib:HttpGetStep.Parameters>
    </nib:HttpGetStep>
  </TestCase.SetupSteps>

  <TestCase.ExecutionSteps>
    <nib:DocumentMergeStep FailOnError="True" OutputCacheKey="inputDocument" RunConcurrently="False">
      <nib:DocumentMergeStep.InputDocuments>
        <nib:RandomLoader BoundingBox="Europe" LinkbackUrlPattern="http://MyProviderService/id={0}" MaxAmount="10" MaxID="100" MinAmount="10" MinID="0" NamePattern="EuropeanObject_{0}" NativeFormat="Svg" RepeatableRandomness="False" UriPrefix="European" />
        <nib:RandomLoader BoundingBox="PacificIslands" LinkbackUrlPattern="http://MyProviderService/id={0}" MaxAmount="10" MaxID="100" MinAmount="10" MinID="0" NamePattern="PacificObject_{0}" NativeFormat="Svg" RepeatableRandomness="False" UriPrefix="Pacific" />
      </nib:DocumentMergeStep.InputDocuments>
    </nib:DocumentMergeStep>
    <nib:PushToSimulatedApp AppKernelCacheKey="provider" ContentFormat="Svg" FailOnError="True" RunConcurrently="False">
      <nib:PushToSimulatedApp.InputDocument>
        <nib:CacheLoader SourceCacheKey="inputDocument" />
      </nib:PushToSimulatedApp.InputDocument>
    </nib:PushToSimulatedApp>
    <nib:GeoFilterStep FailOnError="True" OutputCacheKey="filteredDocument" RunConcurrently="False" SelectionBox="Europe">
      <nib:GeoFilterStep.InputDocument>
        <nib:CacheLoader SourceCacheKey="inputDocument" />
      </nib:GeoFilterStep.InputDocument>
    </nib:GeoFilterStep>
    <nib:DeepCompareStep DepthOfComparision="ID, Geo_2MeterAccuracy, PropertyBag, LinkbackUrl" FailOnError="True" RunConcurrently="False" Timeout="30000" TolerateAdditionalItems="False">
      <nib:DeepCompareStep.ReferenceSource>
        <nib:CacheLoader SourceCacheKey="filteredDocument" />
      </nib:DeepCompareStep.ReferenceSource>
      <nib:DeepCompareStep.InvestigatedSource>
        <nib:SvgWebServiceLoader GeoFilter="Europe" NvgServiceUrl="http://localhost:10000/SvgOutputPort.asmx"/>
      </nib:DeepCompareStep.InvestigatedSource>
    </nib:DeepCompareStep>
  </TestCase.ExecutionSteps>

  <TestCase.CleanupSteps>
    <nib:HttpGetStep FailOnError="True" RunConcurrently="False" Url="http://localhost:10000/Home/StopSvgPolling">
      <nib:HttpGetStep.Parameters>
        <x:String x:Key="PolledAddress">http://localhost:4000/SvgOutputPort.asmx</x:String>
      </nib:HttpGetStep.Parameters>
    </nib:HttpGetStep>
    <nib:KillSimulatedApp AppKernelCacheKey="provider" FailOnError="True" PortNumber="4000" RunConcurrently="False" />
  </TestCase.CleanupSteps>
</TestCase>

这就是它的作用:

  1. 在测试主题上调用Clear操作
  2. 在端口4000上以名为MyProviderService
  3. 的模拟合作伙伴应用程序启动网络服务器
  4. 通过HTTP Get调查模拟合作伙伴
  5. 调用测试主题
  6. 创建一个包含来自两个随机生成内容的地理信息的新文档
  7. 将文档推送到模拟合作伙伴 - 因此测试主题将通过轮询
  8. 获取
  9. 测试在文档上应用地理过滤器
  10. 深度比较步骤将过滤后的文档作为比较基础加载,并通过Web服务加载测试主题的内容
  11. 作为清理工作,它会通过HTTP GET步骤停止轮询并杀死模拟合作伙伴的Web服务器。
  12. Bizunit的强大之处在于它将C#中创建测试的难易程度与智能感知相结合,并且易于在XAML文件中维护/复制它。有关其工作原理的快速简单阅读:http://kevinsmi.wordpress.com/2011/03/22/bizunit-4-0-overview/

4 个答案:

答案 0 :(得分:1)

您可以创建针对每组测试数据重复运行的单个测试,而不是为每组测试数据创建单独的测试:

[TestClass]
public class SampleTests
{
    [TestMethod]
    public void Test()
    {
        for (var i = 0; i < 10; ++i)
            XamlTestManager.ConductTest(i); 
    }
}

您还可以使用DataSource attribute执行数据驱动的测试。这将对数据集中的每一行执行测试。

[TestClass]
public class SampleTests
{
    public TestContext Context { get; set; }

    [TestMethod]
    [DataSource(...)]
    public void Test()
    {
        var someData = Context.DataRow["SomeColumnName"].ToString();
        ...
    }
}

答案 1 :(得分:1)

正如@GeorgeDuckett所说,T4模板可能是要走的路。在我正在开发的应用程序中,我们经常使用它们,包括生成存储库,服务,ViewModel,枚举和最近的单元测试。

它们基本上是用VB或C#编写的代码生成脚本,查看XML文件的目录对于这些类型的模板都没有问题。

如果您确实选择了T4路线,Tangible T4 Editor肯定是必须的,可以免费下载。

以下是T4脚本的一个快速示例,该脚本应该或者非常接近您想要的内容:

<#@ template language="C#" debug="true" hostspecific="true"#>
<#@ output extension="g.cs"#>
[TestClass]
public class SampleTests
{
<#
string[] files = Directory.GetFiles(@"C:\TestFiles", "*.xml");
foreach(string filePath in files)
{
    string fileName = Path.GetFileNameWithoutExtension(filePath);
#>
    [TestMethod]
    public void <#=fileName#>()
    {
        XamlTestManager.ConductTest();
    }
<#
}
#>
}

确保将其放置在扩展名为.tt的文件中,然后在此文件的属性窗口中,确保构建操作为None,自定义工具为TextTemplatingFileGenerator

编辑:从T4模板访问输出目录

在&lt;#@ template ...#&gt;下添加以下两行到T4模板的顶部。行:

<#@ assembly name="EnvDTE" #>
<#@ import namespace="EnvDTE" #>

然后在模板中,您可以访问和使用visual studio API,如下所示:

IServiceProvider serviceProvider = this.Host as IServiceProvider;
DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE;
object[] activeSolutionProjects = dte.ActiveSolutionProjects as object[];

if(activeSolutionProjects != null)
{
    Project project = activeSolutionProjects[0] as Project;
    if(project != null)
    {
        Properties projectProperties = project.Properties;
        Properties configurationProperties = project.ConfigurationManager.ActiveConfiguration.Properties;
        string projectDirectory = Path.GetDirectoryName(project.FullName);  
        string outputPath = configurationProperties.Item("OutputPath").Value.ToString();
        string outputFile = projectProperties.Item("OutputFileName").Value.ToString();

        string outDir = Path.Combine(projectDirectory, outputPath);
        string targetPath = Path.Combine(outDir, outputFile);
    }
}

outDirtargetPath包含输出目录和输出文件的完整路径。

答案 2 :(得分:0)

我实际上并不认为这是构建时代码生成的工作,我认为在这种情况下你应该使用数据属性来驱动测试。

如果您使用xunit,您可以这样做:

public class SampleTests
{
    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(...)]
    [InlineData(N)]
    public void Test(int x)
    {
        XamlTestManager.ConductTest(x);
    }
}

它将根据InlineData属性运行一次测试。另外我相信还有另一个属性,您可以将路径传递给文件,它将使用该文件中的值填充参数...

我认为NUnit有类似功能,但XUnit要好得多,我建议改用XUnit。

答案 3 :(得分:0)

刚才回答“使用T4从XML生成代码”问题。

https://stackoverflow.com/a/8554949/753110

您的要求与我们最初所做的完全匹配(以及导致在该答案中描述的ADM的发现)。

我们目前正致力于基于测试用例的生成,其中测试用例实际上是由测试人员构建的,但是通过代码生成完整的集成测试以支持它们。

如果您想查看以下内容,请为其他示例添加基于XML的自定义生成演示:

https://github.com/abstractiondev/DemoSOCase8552428ABS