【问题标题】:C# Solve repetitive units test functions using C++-style macros or function pointersC# 使用 C++ 风格的宏或函数指针解决重复的单元测试函数
【发布时间】:2016-01-07 14:14:08
【问题描述】:

我正在编写测试代码来验证第 3 方 API。 API 包含所有类型的命令,但在本次讨论中,让我们看一下布尔类型的访问器,如下所示:

// The code is simplified to explain the problem.
// Assume "Enable" does complex items like talking
// to a database, or complex calculations.
Blah item = new Blah();
item.GroupA.Enable = true;
item.GroupB.Enable = true;
// etc, say we have A LOT of "groups".

我当前的单元测试看起来像这样:

public void GroupAEnable()
{
   bool val = true;
   mItem.GroupA.Enable = val;
   bool ret = mItem.GroupA.Enable;
   VerifyEqual(val, ret);

   val = false;
   item.GroupA.Enable = val;
   ret = mItem.GroupA.Enable;
   VerifyEqual(val, ret);
}

public void GroupBEnable()
{
   bool val = true;
   mItem.GroupB.Enable = val;
   bool ret = mItem.GroupB.Enable;
   VerifyEqual(val, ret);

   val = false;
   mItem.GroupB.Enable = val;
   ret = mItem.GroupB.Enable;
   VerifyEqual(val, ret);
}

我必须为数百个“组”重复此操作。

在 C++ 中,我们可能会对其进行宏化处理并执行以下操作:

#define TEST_BOOL(cmd)       \
   bool val = true;          \
   mItem.##cmd## = val;      \
   bool ret = mItem.##cmd##; \
   VerifyEqual(val, ret);    \
   val = false;              \
   mItem.##cmd## = val;      \
   ret = mItem.##cmd##;      \
   VerifyEqual(val, ret)     

public void GroupAEnable()
{
   TEST_BOOL(GroupA.Enable);
}

public void GroupBEnable()
{
   TEST_BOOL(GroupB.Enable);
}

一个 C# 解决方案是创建一个接受 Action 的 TestBool 函数,但这仍然需要为每个组进行大量输入。

public void TestBool(Action setter, Action getter)
{
   ...
}

public void GroupAEnable()
{
   TestBool(x => mItem.GroupA.Enable = x,
            () => mItem.GroupA.Enable);
}

有什么很酷的方法可以简化这个吗?唯一不同的是访问器名称和类型,而测试(即测试方法)是相同的。

【问题讨论】:

  • 我猜在处理糟糕的设计时,这是必要的邪恶。如果它真的只是改变的属性的名称,为什么假设行为不同?你也可以生成代码。
  • C# 不是 C++。事实上,宏也不是非常 C++ —— 它们是严重不赞成的,因为它们是 C 的遗留物。如果您不想重复代码,请将其提取到方法中。您所描述的是动态调用,在 C# 中使用 dynamic 关键字已经支持
  • 另一个 C#/C++ 选项 - 使用表达式或 lambdas(两种语言都支持它们)。这就是大多数测试框架中的断言的工作方式
  • @Ryuu 在 C++ 中的宏真的非常不受欢迎。这种特定的用法会导致一些心脏病发作。您正在尝试做的是两种语言的不良做法。只需使用您要进行的调用创建一个 Func 数组,然后循环调用每个调用。
  • @PanagiotisKanavos 是的,宏在 C# 中不受欢迎,所以我正在寻找替代方案。通过 lambda,它和我最底层的代码 sn-p 一样吗?无论如何,“动态”对我来说是新的......我会检查一下。

标签: c# unit-testing function-pointers


【解决方案1】:

函数。 Sometype 可能需要根据您的要求变为泛型:

void BoringTest(Sometype item)
{
   bool val = true;
   item.Enable = val;
   bool ret = item.Enable;
   VerifyEqual(val, ret);
   val = false;
   item.Enable = val;
   ret =item.Enable;
   VerifyEqual(val, ret);
}
public void GroupAEnable()
{
   BoringTest(mItem.GroupA);
}
public void GroupBEnable()
{
   BoringTest(mItem.GroupB);
}

如果您不关心速度,另一种选择是使用反射。相关:C# - Set Property of a Property with Reflection

dynamic 使用反射,在这里可能会对您有所帮助,但我不完全确定。我已经很久没有使用它了。请注意,动态在运行时而不是编译时失败。

void BoringTest(object i)
{
   dynamic item = i;
   bool val = true;
   item.Enable = val;
   bool ret = item.Enable;
   VerifyEqual(val, ret);
   val = false;
   item.Enable = val;
   ret =item.Enable;
   VerifyEqual(val, ret);
}
public void GroupAEnable()
{
   BoringTest(mItem.GroupA);
}
public void GroupBEnable()
{
   BoringTest(mItem.GroupB);
}

第三种选择是使用中介,例如 xml 和 xslt,或者您的公司喜欢的任何东西,来生成整个测试文件。

【讨论】:

  • 每个组都是一个独特的界面,所以我认为这行不通。但我会看看反射是什么。谢谢!
  • 反射允许你在函数中输入一个字符串“GroupA”,然后该函数使用反射在mItem上找到属性GroupA。具体以GroupA的实际类型为准。
  • dynamic 是一种非常好的方法 - 在功能上与我的答案基本相同,但更简洁。
  • 感谢彼得和@antiduh。由于速度对我来说并不重要,看起来反射(和动态)对我有用,而其他建议(XML、表达式)将来可能会派上用场来处理其他情况。
【解决方案2】:

尝试使用反射来查找和调用您正在寻找的属性。

这是一个快速而肮脏的示例,大致完成了您想要做的事情:

public static class Program
{
    [STAThread]
    static void Main()
    {
        NeedsTesting target = new NeedsTesting();

        DoTest( target );
    }

    private static void DoTest(NeedsTesting target)
    {
        Type type = typeof( NeedsTesting );
        PropertyInfo[] properties;
        int count = 0;

        properties = type.GetProperties();

        foreach( PropertyInfo property in properties )
        {
            if( property.Name.StartsWith( "Group" ) )
            {
                count++;
                TestProperty( target, property );
            }
        }

        if( count != 5 )
        {
            VerifyEquals( false, true, "Did not find all 5 properties to test" );
        }
    }

    private static void TestProperty( NeedsTesting target, PropertyInfo property )
    {
        bool result;

        property.SetValue( target, true );
        result = (bool)property.GetValue( target );
        VerifyEquals( result, true, string.Format("Property '{0}' failed to retain a 'true' value.", property.Name ) );

        property.SetValue( target, false );
        result = (bool)property.GetValue( target );
        VerifyEquals( result, false, string.Format( "Property '{0}' failed to retain a 'false' value.", property.Name ) );
    }

    private static void VerifyEquals( bool left, bool right, string message )
    {
        if( left != right )
        {
            throw new Exception(
                 string.Format(
                    "Unit test failed - values were not equal:\r\n" +
                    "   left: {0}\r\n" +
                    "  right: {1}\r\n" +
                    "Message:\r\n" +
                    "{2}",
                    left,
                    right,
                    message
                )
            );
        }
    }
}

public class NeedsTesting
{
    private bool groupEValue;

    public bool GroupA { get; set; }

    public bool GroupB { get; set; }

    public bool GroupC { get; set; }

    public bool GroupD { get; set; }

    public bool GroupE
    {
        get
        {
            return this.groupEValue;
        }
        set
        {
            // Oops, this one is broken.
            value = false;
        }
    }
}

【讨论】:

    【解决方案3】:

    这是一种基于表达式的方法:

    public static void VerifyMemberEqualsValueAfterSetting<TValue>(
        Expression<Func<TValue>> memberExpression,
        TValue value)
    {
        var member = (MemberExpression) memberExpression.Body;
        var parameter = Expression.Parameter(typeof (TValue));
        var assignExpresison = Expression.Assign(member, parameter);
    
        Expression.Lambda<Action<TValue>>(assignExpresison, parameter).Compile()(value);
    
        var after = Expression.Lambda<Func<TValue>>(member, null).Compile()();
        VerifyEqual(value,after);
    }
    

    然后像这样使用它:

    VerifyMemberEqualsValueAfterSetting(()=> mItem.GroupA.Enabled, true);
    VerifyMemberEqualsValueAfterSetting(()=> mItem.GroupA.Enabled, false);
    

    或者更简单:

    public static void VerifyMemberEqualsValueAfterSetting<TValue>(
        Expression<Func<TValue>> memberExpression,
        params TValue[] values)
    {
        var member = (MemberExpression) memberExpression.Body;
        var parameter = Expression.Parameter(typeof (TValue));
        var assignExpresison = Expression.Assign(member, parameter);
    
        var setter =Expression.Lambda<Action<TValue>>(assignExpresison, parameter).Compile();
    
        var getter = Expression.Lambda<Func<TValue>>(member, null).Compile();
        foreach(var value in values)
        {
             setter(value);
             VerifyEqual(value,getter(value));
        }
    }
    

    那就只能写了

    VerifyMemberEqualsValueAfterSetting(()=> mItem.GroupA.Enabled, true, false);
    VerifyMemberEqualsValueAfterSetting(()=> whatEver.Count, 1, 2, 3);
    

    【讨论】:

    • 哦,很酷,您可以同时制作一组并从一个 lambda 中获取。感谢您让我接触到 Expressions。
    • 不客气,你可以把它归结为更多,更新我的答案。
    猜你喜欢
    • 1970-01-01
    • 2015-02-18
    • 2019-05-22
    • 2014-11-26
    • 2015-03-22
    • 1970-01-01
    • 1970-01-01
    • 2017-10-11
    相关资源
    最近更新 更多