.net - ترجمة - كيف تقوم وحدة اختبار طرق خاصة؟




vote ترجمة (20)

أعمل على بناء مكتبة للفصول الدراسية التي سيكون لها بعض الأساليب العامة والخاصة. أريد أن أكون قادراً على وحدة اختبار الأساليب الخاصة (غالباً أثناء تطوير ، ولكن أيضاً قد يكون مفيداً لإعادة بناء في المستقبل).

ما هي الطريقة الصحيحة للقيام بذلك؟


أريد إنشاء مثال رمز واضح هنا يمكنك استخدامه في أي فئة تريد اختبار الطريقة الخاصة بها.

في فئة الاختبار الخاصة بك ، قم فقط بتضمين هذه الطرق ثم استخدمها كما هو محدد.

  /**
   *
   * @var Class_name_of_class_you_want_to_test_private_methods_in
   * note: the actual class and the private variable to store the 
   * class instance in, should at least be different case so that
   * they do not get confused in the code.  Here the class name is
   * is upper case while the private instance variable is all lower
   * case
   */
  private $class_name_of_class_you_want_to_test_private_methods_in;

  /**
   * This uses reflection to be able to get private methods to test
   * @param $methodName
   * @return ReflectionMethod
   */
  protected static function getMethod($methodName) {
    $class = new ReflectionClass('Class_name_of_class_you_want_to_test_private_methods_in');
    $method = $class->getMethod($methodName);
    $method->setAccessible(true);
    return $method;
  }

  /**
   * Uses reflection class to call private methods and get return values.
   * @param $methodName
   * @param array $params
   * @return mixed
   *
   * usage:     $this->_callMethod('_someFunctionName', array(param1,param2,param3));
   *  {params are in
   *   order in which they appear in the function declaration}
   */
  protected function _callMethod($methodName, $params=array()) {
    $method = self::getMethod($methodName);
    return $method->invokeArgs($this->class_name_of_class_you_want_to_test_private_methods_in, $params);
  }

$ this -> _ callMethod ('_ someFunctionName'، array (param1، param2، param3))؛

ما عليك سوى إصدار المعلمات بالترتيب الذي تظهر به في الوظيفة الخاصة الأصلية


أعتقد أن السؤال الأساسي الذي يجب طرحه هو لماذا تحاول اختبار الطريقة الخاصة في المقام الأول. هذه هي رائحة الشفرة التي تحاول فيها اختبار الطريقة الخاصة من خلال الواجهة العامة لهذه الفئة ، في حين أن هذه الطريقة خاصة لسبب ما لأنها تفاصيل تنفيذ. يجب أن يهتم المرء فقط بسلوك الواجهة العامة وليس على كيفية تنفيذه تحت الأغطية.

إذا كنت أرغب في اختبار سلوك الطريقة الخاصة ، فباستخدام عمليات إعادة التصنيع الشائعة ، يمكنني استخراج الشفرة إلى فئة أخرى (ربما مع مستوى رؤية الحزمة ، لذا تأكد من أنها ليست جزءًا من واجهة برمجة التطبيقات العامة). يمكنني بعد ذلك اختبار سلوكه في عزلة.

ويعني منتج إعادة البيع أن الطريقة الخاصة أصبحت الآن فئة منفصلة أصبحت متعاونًا مع الطبقة الأصلية. سيصبح سلوكه مفهومًا جيدًا من خلال اختبارات الوحدة الخاصة به.

يمكنني بعد ذلك أن أسخر من سلوكه عندما أحاول اختبار الصف الأصلي حتى أتمكن من التركيز على اختبار سلوك الواجهة العامة تلك الطبقة بدلا من الاضطرار لاختبار انفجار اندماجي للواجهة العامة وسلوك جميع أساليبها الخاصة .

أرى أن هذا مشابه لقيادة السيارة. عندما أقود سيارة لا أقودها باستخدام غطاء المحرك لأتمكن من رؤية المحرك يعمل. أنا أعتمد على الواجهة التي توفرها السيارة ، وهي عداد السرعة وعداد السرعة لمعرفة المحرك يعمل. أنا أعتمد على حقيقة أن السيارة تتحرك بالفعل عندما أضغط على دواسة الوقود. إذا كنت أرغب في اختبار المحرك ، يمكنني إجراء فحوصات على ذلك بشكل منعزل. :د

وبالطبع ، قد يكون اختبار الطرق الخاصة بشكل مباشر هو الحل الأخير إذا كان لديك تطبيق قديم ولكنني أفضل أن يتم إعادة بناء الشفرة القديمة لتمكين إجراء اختبار أفضل. كتب مايكل فيشرز كتابًا رائعًا حول هذا الموضوع بالذات. http://www.amazon.co.uk/Working-Effectively-Legacy-Robert-Martin/dp/0131177052


أنا مندهش لم يقل أحد حتى الآن ، لكن الحل الذي استخدمته هو جعل طريقة ثابتة داخل الفصل لاختبار نفسه. يتيح لك هذا الوصول إلى كل شيء عام وخاص لاختباره.

علاوة على ذلك ، في لغة البرمجة النصية (مع قدرات OO ، مثل Python و Ruby و PHP) ، يمكنك إجراء اختبار الملف نفسه عند التشغيل. طريقة سريعة لطيفة للتأكد من أن التغييرات التي أجريتها لم تكسر أي شيء. من الواضح أن هذا يجعل الحل قابلاً للتحليل لاختبار جميع الفصول الدراسية: فقط قم بتشغيلها كلها. (يمكنك القيام بذلك أيضًا بلغات أخرى باستخدام مفتاح فارغ يعمل دائمًا على إجراء اختباراته أيضًا).


إذا كنت تريد اختبار الوحدة بطريقة خاصة ، فقد يكون هناك خطأ ما. اختبارات الوحدة (بشكل عام) تعني اختبار واجهة الصف ، أي طرقها العامة (والمحمية). يمكنك بالطبع "اختراق" حل لهذا (حتى لو كان فقط من خلال جعل الطرق العامة) ، ولكن قد تحتاج أيضا إلى النظر في:

  1. إذا كانت الطريقة التي ترغب في اختبارها تستحق الاختبار حقًا ، فقد يكون من المفيد نقلها إلى الفصل الخاص بها.
  2. إضافة المزيد من الاختبارات إلى الطرق العامة التي تستدعي الطريقة الخاصة ، واختبار وظائف الطريقة الخاصة. (كما أشار المعلقون ، يجب عليك القيام بذلك فقط إذا كانت وظيفة هذه الطرق الخاصة جزءًا بالفعل في الواجهة العامة. إذا كانت تؤدي بالفعل وظائف مخفية عن المستخدم (أي اختبار الوحدة) ، فمن المحتمل أن يكون ذلك سيئًا).

الأنواع الخاصة ، والأعضاء الداخليين والأفراد هم السبب في ذلك لسبب ما ، وغالبا ما لا ترغب في الفوضى معهم مباشرة. وإذا فعلت ذلك ، فستكون فرصتك في وقت لاحق ، لأنه ليس هناك ما يضمن أن الأشخاص الذين أنشأوا هذه التجمعات سيبقون عمليات التنفيذ الخاصة / الداخلية على هذا النحو.

ولكن في بعض الأحيان ، عند القيام ببعض الاختراقات / استكشاف تجميعات التجميع أو الأطراف الثالثة ، فقد انتهى بي الأمر برغبتي في تهيئة طبقة خاصة أو فصل دراسي مع مُنشئ خاص أو داخلي. أو ، في بعض الأحيان ، عند التعامل مع المكتبات القديمة التي تم تجميعها مسبقًا والتي لا يمكنني تغييرها - انتهى بي الأمر بكتابة بعض الاختبارات ضد طريقة خاصة.

وهكذا ولدت في AccessPrivateWrapper - http://amazedsaint.blogspot.com/2010/05/accessprivatewrapper-c-40-dynamic.html - إنها فئة ملحقة سريعة ستجعل المهمة سهلة باستخدام الميزات الديناميكية C # 4.0 والتأمل.

يمكنك إنشاء أنواع داخلية / خاصة مثل

    //Note that the wrapper is dynamic
    dynamic wrapper = AccessPrivateWrapper.FromType
        (typeof(SomeKnownClass).Assembly,"ClassWithPrivateConstructor");

    //Access the private members
    wrapper.PrivateMethodInPrivateClass();

حسنا يمكنك اختبار وحدة الطريقة الخاصة بطريقتين

  1. يمكنك إنشاء مثيل لفئة PrivateObject بناء الجملة كما يلي

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
  2. يمكنك استخدام التفكير.

    PrivateClass obj = new PrivateClass(); // Class containing private obj
    Type t = typeof(PrivateClass); 
    var x = t.InvokeMember("PrivateFunc", 
        BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public |  
            BindingFlags.Instance, null, obj, new object[] { 5 });
    

في CodeProject ، هناك مقالة تناقش بإيجاز إيجابيات وسلبيات اختبار الطرق الخاصة. ومن ثم يوفر بعض التعليمات البرمجية للانعكاس للوصول إلى الطرق الخاصة (على غرار الرمز الذي يوفره ماركوس أعلاه). المشكلة الوحيدة التي وجدتها مع العينة هي أن الشفرة لا تأخذ في الحسبان الأساليب المحملة فوق طاقتها.

يمكنك إيجاد المقالة هنا:

http://www.codeproject.com/KB/cs/testnonpublicmembers.aspx


في الحالات النادرة التي كنت أرغب فيها في اختبار الوظائف الخاصة ، قمت عادة بتعديلها ليتم حمايتها بدلاً من ذلك ، ولقد كتبت فئة فرعية مع وظيفة غلاف عام.

الدرجة:

...

protected void APrivateFunction()
{
    ...
}

...

فئة فرعية للاختبار:

...

[Test]
public void TestAPrivateFunction()
{
    APrivateFunction();
    //or whatever testing code you want here
}

...

قد لا يكون من المفيد اختبار الطرق الخاصة. ومع ذلك ، أود أيضًا أحيانًا الاتصال بأساليب خاصة من طرق الاختبار. معظم الوقت من أجل منع تكرار الكود لإنشاء بيانات الاختبار ...

توفر Microsoft آليات جهازي لهذا:

يمكنهم الدخول

  • انتقل إلى شفرة المصدر لتعريف الصف
  • انقر بزر الماوس الأيمن على اسم الفصل
  • اختر "إنشاء Private Accessor"
  • اختر المشروع الذي يجب إنشاء أداة accessor => ستنتهي بفئة جديدة باسم foo_accessor. سيتم إنشاء هذه الفئة ديناميكيًا أثناء عملية التجميع ، كما ستخصّص جميع الأعضاء المتاحين للجمهور.

ومع ذلك ، فإن الآلية تكون في بعض الأحيان مستعصية بعض الشيء عندما يتعلق الأمر بالتغييرات في واجهة الطبقة الأصلية. لذلك ، في معظم الأحيان أتجنب استخدام هذا.

فئة PrivateObject الطريقة الأخرى هي استخدام Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject

// Wrap an already existing instance
PrivateObject accessor = new PrivateObject( objectInstanceToBeWrapped );

// Retrieve a private field
MyReturnType accessiblePrivateField = (MyReturnType) accessor.GetField( "privateFieldName" );

// Call a private method
accessor.Invoke( "PrivateMethodName", new Object[] {/* ... */} );

قم بتعريفها internal ، ثم استخدم InternalsVisibleToAttribute للسماح لتجميع اختبار الوحدة الخاص بك برؤيتها.


لقد استخدمت أيضًا طريقة InternalsVisibleToAttribute. تجدر الإشارة أيضًا إلى أنه إذا كنت تشعر بعدم الارتياح لجعل الطرق الداخلية الخاصة بك سابقًا في الداخل لتحقيق هذا ، فربما يجب ألا تكون موضوعًا لاختبارات وحدة مباشرة على أي حال.

بعد كل شيء ، فأنت تختبر سلوك صفك ، بدلاً من التنفيذ المحدد - يمكنك تغيير هذا الأخير دون تغيير السابق ولا بد من اجتياز اختباراتك.


لمن يريد تشغيل أساليب خاصة دون كل هذه السعادة والفوضى. هذا يعمل مع أي إطار اختبار وحدة باستخدام أي شيء سوى انعكاس قديم جيد.

public class ReflectionTools
{
    // If the class is non-static
    public static Object InvokePrivate(Object objectUnderTest, string method, params object[] args)
    {
        Type t = objectUnderTest.GetType();
        return t.InvokeMember(method,
            BindingFlags.InvokeMethod |
            BindingFlags.NonPublic |
            BindingFlags.Instance |
            BindingFlags.Static,
            null,
            objectUnderTest,
            args);
    }
    // if the class is static
    public static Object InvokePrivate(Type typeOfObjectUnderTest, string method, params object[] args)
    {
        MemberInfo[] members = typeOfObjectUnderTest.GetMembers(BindingFlags.NonPublic | BindingFlags.Static);
        foreach(var member in members)
        {
            if (member.Name == method)
            {
                return typeOfObjectUnderTest.InvokeMember(method, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod, null, typeOfObjectUnderTest, args);
            }
        }
        return null;
    }
}

Then in your actual tests, you can do something like this:

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    typeof(StaticClassOfMethod), 
    "PrivateMethod"), 
  "Expected Result");

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    new ClassOfMethod(), 
    "PrivateMethod"), 
  "Expected Result");

يجب ألا تختبر الطرق الخاصة للرمز الخاص بك في المقام الأول. يجب أن تختبر "الواجهة العامة" أو واجهة برمجة التطبيقات (API) ، الأشياء العامة لصفوفك. واجهة برمجة التطبيقات هي جميع الطرق العامة التي تعرضها للمتصلين الخارجيين.

والسبب هو أنه بمجرد بدء اختبار الأساليب الخاصة والداخلية لفصلك ، فإنك تقترب من تنفيذ فصلك (الأشياء الخاصة) إلى اختباراتك. هذا يعني أنه عندما تقرر تغيير تفاصيل التنفيذ ، سيتعين عليك أيضًا تغيير اختباراتك.

يجب عليك لهذا السبب تجنب استخدام InternalsVisibleToAtrribute.

ها هو حديث عظيم من إيان كوبر الذي يغطي هذا الموضوع: إيان كوبر: TDD ، أين ذهب كل هذا بشكل خاطئ


يمتلك اختبار MS ميزة رائعة مضمنة تجعل الأعضاء الخاصة والأساليب المتوفرة في المشروع عن طريق إنشاء ملف يسمى VSCodeGenAccessors

[System.Diagnostics.DebuggerStepThrough()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
    internal class BaseAccessor
    {

        protected Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject m_privateObject;

        protected BaseAccessor(object target, Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
        {
            m_privateObject = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(target, type);
        }

        protected BaseAccessor(Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
            :
                this(null, type)
        {
        }

        internal virtual object Target
        {
            get
            {
                return m_privateObject.Target;
            }
        }

        public override string ToString()
        {
            return this.Target.ToString();
        }

        public override bool Equals(object obj)
        {
            if (typeof(BaseAccessor).IsInstanceOfType(obj))
            {
                obj = ((BaseAccessor)(obj)).Target;
            }
            return this.Target.Equals(obj);
        }

        public override int GetHashCode()
        {
            return this.Target.GetHashCode();
        }
    }

مع الطبقات التي تنبع من BaseAccessor

مثل

[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class SomeClassAccessor : BaseAccessor
{

    protected static Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType m_privateType = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType(typeof(global::Namespace.SomeClass));

    internal SomeClassAccessor(global::Namespace.Someclass target)
        : base(target, m_privateType)
    {
    }

    internal static string STATIC_STRING
    {
        get
        {
            string ret = ((string)(m_privateType.GetStaticField("STATIC_STRING")));
            return ret;
        }
        set
        {
            m_privateType.SetStaticField("STATIC_STRING", value);
        }
    }

    internal int memberVar    {
        get
        {
            int ret = ((int)(m_privateObject.GetField("memberVar")));
            return ret;
        }
        set
        {
            m_privateObject.SetField("memberVar", value);
        }
    }

    internal int PrivateMethodName(int paramName)
    {
        object[] args = new object[] {
            paramName};
        int ret = (int)(m_privateObject.Invoke("PrivateMethodName", new System.Type[] {
                typeof(int)}, args)));
        return ret;
    }

A way to do this is to have your method protected and write a test fixture which inherits your class to be tested. This way, you are nor turning your method public , but you enable the testing.


Also note that the InternalsVisibleToAtrribute has a requirement that your assembly be strong named , which creates it's own set of problems if you're working in a solution that had not had that requirement before. I use the accessor to test private methods. See this question that for an example of this.


Here's an example, first the method signature:

private string[] SplitInternal()
{
    return Regex.Matches(Format, @"([^/\[\]]|\[[^]]*\])+")
                        .Cast<Match>()
                        .Select(m => m.Value)
                        .Where(s => !string.IsNullOrEmpty(s))
                        .ToArray();
}

Here's the test:

/// <summary>
///A test for SplitInternal
///</summary>
[TestMethod()]
[DeploymentItem("Git XmlLib vs2008.dll")]
public void SplitInternalTest()
{
    string path = "pair[path/to/@Key={0}]/Items/Item[Name={1}]/Date";
    object[] values = new object[] { 2, "Martin" };
    XPathString xp = new XPathString(path, values);

    PrivateObject param0 = new PrivateObject(xp);
    XPathString_Accessor target = new XPathString_Accessor(param0);
    string[] expected = new string[] {
        "pair[path/to/@Key={0}]",
        "Items",
        "Item[Name={1}]",
        "Date"
    };
    string[] actual;
    actual = target.SplitInternal();
    CollectionAssert.AreEqual(expected, actual);
}

I use PrivateObject class. But as mentioned previously better to avoid testing private methods.

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);

You could also declare it as public or internal (with InternalsVisibleToAttribute) while building in debug-Mode:

    /// <summary>
    /// This Method is private.
    /// </summary>
#if DEBUG
    public
#else
    private
#endif
    static string MyPrivateMethod()
    {
        return "false";
    }

It bloats the code, but it will be private in a release build.


You could generate the test method for the private method from Visual studio 2008. When you create a unit test for a private method, a Test References folder is added to your test project and an accessor is added to that folder. The accessor is also referred to in the logic of the unit test method. This accessor allows your unit test to call private methods in the code that you are testing. For details have a look at

http://msdn.microsoft.com/en-us/library/bb385974.aspx





private