كيفية وحدة اختبار كائن مع استعلامات قاعدة البيانات

StackOverflow https://stackoverflow.com/questions/30710

  •  09-06-2019
  •  | 
  •  

سؤال

لقد سمعت أن اختبار الوحدة "رائع تمامًا" و"رائع حقًا" و"جميع أنواع الأشياء الجيدة" ولكن 70% أو أكثر من ملفاتي تتضمن الوصول إلى قاعدة البيانات (بعضها للقراءة والبعض الآخر للكتابة) ولست متأكدًا من ذلك لكتابة اختبار وحدة لهذه الملفات.

أنا أستخدم PHP وPython ولكن أعتقد أنه سؤال ينطبق على معظم/جميع اللغات التي تستخدم الوصول إلى قاعدة البيانات.

هل كانت مفيدة؟

المحلول

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

من أجل إعداد كائناتك للسخرية، ربما تحتاج إلى استخدام نوع من الانعكاس لنمط حقن التحكم/التبعية، كما في الكود الزائف التالي:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

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

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

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

نصائح أخرى

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

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

في مشاريعي في لغة C#، أستخدم NHibernate مع طبقة بيانات منفصلة تمامًا.تعيش كائناتي في نموذج المجال الأساسي ويمكن الوصول إليها من طبقة التطبيق الخاصة بي.تتحدث طبقة التطبيق مع كل من طبقة البيانات وطبقة نموذج المجال.

تُسمى طبقة التطبيق أيضًا أحيانًا "طبقة الأعمال".

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

هناك خيار آخر يتمثل في استخدام السخرية/بذرة.

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

على سبيل المثال:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

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

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

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

ربما يمكنني أن أعطيك لمحة عن تجربتنا عندما بدأنا النظر في وحدة اختبار عملية الطبقة المتوسطة لدينا والتي تضمنت عددًا كبيرًا من عمليات SQL "منطق الأعمال".

قمنا أولاً بإنشاء طبقة تجريد سمحت لنا "بفتح" أي اتصال معقول بقاعدة البيانات (في حالتنا، قمنا ببساطة بدعم اتصال واحد من نوع ODBC).

بمجرد تنفيذ ذلك، أصبحنا قادرين على القيام بشيء مثل هذا في الكود الخاص بنا (نحن نعمل بلغة C++، لكنني متأكد من أنك حصلت على الفكرة):

GetDatabase().ExecuteSQL( "INSERT INTO foo ( blah, blah )")

في وقت التشغيل العادي، يقوم GetDatabase() بإرجاع كائن يغذي كل SQL لدينا (بما في ذلك الاستعلامات)، عبر ODBC مباشرة إلى قاعدة البيانات.

بدأنا بعد ذلك في النظر إلى قواعد البيانات الموجودة في الذاكرة - ويبدو أن أفضلها على الإطلاق هو SQLite.(http://www.sqlite.org/index.html).من السهل جدًا إعداده واستخدامه، وقد أتاح لنا فئة فرعية وتجاوز GetDatabase() لإعادة توجيه SQL إلى قاعدة بيانات في الذاكرة تم إنشاؤها وتدميرها لكل اختبار يتم إجراؤه.

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

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

من الواضح أن تجاربنا تركزت حول بيئة تطوير C++، لكنني متأكد من أنه ربما يمكنك الحصول على شيء مماثل يعمل ضمن PHP/Python.

أتمنى أن يساعدك هذا.

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

الخيارات المتاحة لك:

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

    class Database {
     public Result query(String query) {... real db here ...}
    }

    Class MockDatabase يمتد قاعدة البيانات {public result query (Query Query) {return "result mock result" ؛}}

    كائن الفئة thatatusesdb {public ObjectThatusesDB (قاعدة البيانات db) {this.database = db ؛}}

    الآن في الإنتاج، تستخدم قاعدة بيانات عادية ولجميع الاختبارات، ما عليك سوى حقن قاعدة البيانات الوهمية التي يمكنك إنشاؤها خصيصًا.

  • لا تستخدم قاعدة البيانات على الإطلاق خلال معظم التعليمات البرمجية (هذه ممارسة سيئة على أي حال).قم بإنشاء كائن "قاعدة بيانات" والذي بدلاً من العودة بالنتائج سيُرجع كائنات عادية (على سبيل المثال.سيعود User بدلاً من الصف {name: "marcin", password: "blah"}) اكتب جميع اختباراتك مع إنشاء مخصص حقيقي الكائنات وكتابة اختبار كبير يعتمد على قاعدة بيانات تتأكد من أن هذا التحويل يعمل بشكل جيد.

بالطبع هذه الأساليب لا يستبعد بعضها بعضًا ويمكنك مزجها ومطابقتها حسب حاجتك.

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

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

لم أفعل هذا مطلقًا في PHP ولم أستخدم Python مطلقًا، ولكن ما تريد القيام به هو الاستهزاء بالاستدعاءات إلى قاعدة البيانات.للقيام بذلك يمكنك تنفيذ بعض IoC سواء أكانت أداة تابعة لجهة خارجية أو كنت تديرها بنفسك، فيمكنك تنفيذ بعض الإصدارات الوهمية من المتصل بقاعدة البيانات حيث يمكنك التحكم في نتيجة تلك المكالمة المزيفة.

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

نأمل أن يكون ذلك مفيدًا، إذا لم يكن هناك أي شيء آخر، فلديك بعض المصطلحات للبحث عنها الآن.

أتفق مع المنشور الأول - يجب تجريد الوصول إلى قاعدة البيانات إلى طبقة DAO التي تنفذ الواجهة.بعد ذلك، يمكنك اختبار المنطق الخاص بك مقابل تطبيق كعب الروتين لطبقة DAO.

يمكنك استخدام الأطر الساخرة لاستخراج محرك قاعدة البيانات.لا أعرف ما إذا كان PHP/Python قد حصل على بعض الخيارات ولكن بالنسبة للغات المكتوبة (C# وJava وما إلى ذلك) هناك الكثير من الخيارات

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

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

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

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

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

آسف، ليس لدي أي أمثلة محددة للتعليمات البرمجية لـ PHP/Python، ولكن إذا كنت تريد رؤية مثال .NET، فلدي بريد الذي يصف التقنية التي استخدمتها لإجراء نفس الاختبار.

يمكن أن يمثل إعداد بيانات الاختبار لاختبارات الوحدة تحديًا.

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top