การทดสอบหน่วย django โดยไม่มีฐานข้อมูล


127

มีความเป็นไปได้ไหมที่จะเขียน django unittests โดยไม่ต้องตั้งค่า db? ฉันต้องการทดสอบตรรกะทางธุรกิจที่ไม่ต้องใช้ฐานข้อมูลในการตั้งค่า และในขณะที่ตั้งค่าฐานข้อมูลได้รวดเร็ว แต่ฉันก็ไม่จำเป็นต้องใช้มันในบางสถานการณ์


ฉันสงสัยว่านั่นเป็นเรื่องจริงหรือไม่ db จะถูกเก็บไว้ในหน่วยความจำ + หากคุณไม่มีโมเดลใด ๆ จะไม่มีการทำงานกับ db ดังนั้นหากคุณไม่ต้องการก็อย่าตั้งค่าโมเดล
Torsten Engelbrecht

3
ฉันมีแบบจำลอง แต่สำหรับการทดสอบเหล่านั้นไม่เกี่ยวข้อง และฐานข้อมูลจะไม่ถูกเก็บไว้ในหน่วยความจำ แต่สร้างขึ้นใน mysql เพื่อจุดประสงค์นี้โดยเฉพาะ ไม่ใช่ว่าฉันต้องการสิ่งนี้ .. บางทีฉันอาจกำหนดค่า django ให้ใช้ db ในหน่วยความจำสำหรับการทดสอบ คุณรู้วิธีการทำเช่นนี้หรือไม่?
paweloque

โอ้ฉันขอโทษ. ฐานข้อมูลในหน่วยความจำเป็นเพียงกรณีเดียวเมื่อคุณใช้ฐานข้อมูล SQLite ยกเว้นสิ่งนี้ฉันไม่เห็นวิธีหลีกเลี่ยงการสร้างฐานข้อมูลทดสอบ ไม่มีอะไรเกี่ยวกับเรื่องนี้ในเอกสาร + ฉันไม่เคยรู้สึกว่าจำเป็นต้องหลีกเลี่ยง
Torsten Engelbrecht

3
คำตอบที่ยอมรับไม่ได้ผลกับฉัน สิ่งนี้ทำงานได้อย่างสมบูรณ์แบบ: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

คำตอบ:


122

คุณสามารถ subclass DjangoTestSuiteRunner และแทนที่วิธีการ setup_databases และ teardown_databases เพื่อส่งผ่าน

สร้างไฟล์การตั้งค่าใหม่และตั้งค่า TEST_RUNNER เป็นคลาสใหม่ที่คุณเพิ่งสร้าง จากนั้นเมื่อคุณทำการทดสอบให้ระบุไฟล์การตั้งค่าใหม่ด้วยแฟล็ก --settings

นี่คือสิ่งที่ฉันทำ:

สร้างนักวิ่งชุดทดสอบที่กำหนดเองคล้ายกับสิ่งนี้:

from django.test.simple import DjangoTestSuiteRunner

class NoDbTestRunner(DjangoTestSuiteRunner):
  """ A test runner to test without database creation """

  def setup_databases(self, **kwargs):
    """ Override the database creation defined in parent class """
    pass

  def teardown_databases(self, old_config, **kwargs):
    """ Override the database teardown defined in parent class """
    pass

สร้างการตั้งค่าแบบกำหนดเอง:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

เมื่อคุณเรียกใช้การทดสอบให้เรียกใช้ดังต่อไปนี้โดยตั้งค่าสถานะ --settings เป็นไฟล์การตั้งค่าใหม่ของคุณ:

python manage.py test myapp --settings='no_db_settings'

UPDATE: เมษายน / 2561

ตั้งแต่ Django 1.8 โมดูลถูกย้ายไปที่.django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

สำหรับข้อมูลเพิ่มเติมโปรดตรวจสอบส่วนเอกสารอย่างเป็นทางการเกี่ยวกับนักวิ่งทดสอบที่กำหนดเอง


2
ข้อผิดพลาดนี้เกิดขึ้นเมื่อคุณมีการทดสอบที่ต้องการธุรกรรมฐานข้อมูล เห็นได้ชัดว่าถ้าคุณไม่มี DB คุณจะไม่สามารถเรียกใช้การทดสอบเหล่านั้นได้ คุณควรทำการทดสอบแยกกัน หากคุณเพิ่งเรียกใช้การทดสอบของคุณโดยใช้การทดสอบ python Manage.py --settings = new_settings.py การทดสอบอื่น ๆ ทั้งหมดจากแอปอื่น ๆ ที่อาจต้องใช้ฐานข้อมูล
mohi666

5
โปรดทราบว่าคุณจะต้องขยาย SimpleTestCase แทน TestCase สำหรับคลาสทดสอบของคุณ TestCase ต้องการฐานข้อมูล
Ben Roberts

9
หากคุณไม่ต้องการใช้ไฟล์การตั้งค่าใหม่คุณสามารถระบุ TestRunner ใหม่ในบรรทัดคำสั่งด้วย--testrunnerตัวเลือก
Bran Handley

26
ตอบโจทย์มาก !! ใน django 1.8 จาก django.test.simple import DjangoTestSuiteRunner ถูกเปลี่ยนจาก django.test.runner import DiscoverRunner หวังว่าจะช่วยใครสักคน!
Josh Brown

2
ใน Django 1.8 ขึ้นไปสามารถแก้ไขโค้ดด้านบนได้เล็กน้อย คำสั่ง import สามารถเปลี่ยนเป็น: from django.test.runner import DiscoverRunner ตอนนี้ NoDbTestRunner ต้องขยายคลาส DiscoverRunner
Aditya Satyavada

78

โดยทั่วไปการทดสอบในแอปพลิเคชันสามารถแบ่งได้เป็นสองประเภท

  1. การทดสอบหน่วยการทดสอบเหล่านี้จะทดสอบส่วนย่อยของรหัสแต่ละรายการในภาวะไข้แดดและไม่จำเป็นต้องไปที่ฐานข้อมูล
  2. กรณีทดสอบการรวมซึ่งไปที่ฐานข้อมูลจริงและทดสอบตรรกะแบบบูรณาการอย่างสมบูรณ์

Django รองรับการทดสอบทั้งหน่วยและการรวม

การทดสอบหน่วยไม่จำเป็นต้องมีการติดตั้งและรื้อฐานข้อมูลและสิ่งเหล่านี้เราควรจะได้รับมรดกจากSimpleTestCase

from django.test import SimpleTestCase


class ExampleUnitTest(SimpleTestCase):
    def test_something_works(self):
        self.assertTrue(True)

สำหรับกรณีการทดสอบการรวมที่สืบทอดมาจาก TestCase จะสืบทอดมาจาก TransactionTestCase และจะตั้งค่าและแยกฐานข้อมูลก่อนเรียกใช้การทดสอบแต่ละครั้ง

from django.test import TestCase


class ExampleIntegrationTest(TestCase):
    def test_something_works(self):
        #do something with database
        self.assertTrue(True)

กลยุทธ์นี้จะช่วยให้มั่นใจได้ว่าฐานข้อมูลที่สร้างขึ้นและถูกทำลายสำหรับกรณีทดสอบที่เข้าถึงฐานข้อมูลเท่านั้นดังนั้นการทดสอบจะมีประสิทธิภาพมากขึ้น


37
สิ่งนี้อาจทำให้การทดสอบการวิ่งมีประสิทธิภาพมากขึ้น แต่โปรดทราบว่านักวิ่งทดสอบยังคงสร้างฐานข้อมูลการทดสอบเกี่ยวกับการเริ่มต้น
กุฏ

6
ง่ายกว่ามากสำหรับคำตอบที่เลือก ขอบคุณมาก!
KFunk

1
@monkut ไม่ ... ถ้าคุณมีคลาส SimpleTestCase เท่านั้นผู้ทดสอบไม่ได้วิ่งอะไรเลยดูโครงการนี้
Claudio Santos

Django จะยังคงพยายามสร้างฐานข้อมูลทดสอบแม้ว่าคุณจะใช้ SimpleTestCase ก็ตาม ดูคำถามนี้
Marko Prcać

การใช้ SimpleTestCase ใช้ได้กับวิธีการทดสอบยูทิลิตีหรือตัวอย่างข้อมูลและไม่ได้ใช้หรือสร้างฐานข้อมูลทดสอบ สิ่งที่ฉันต้องการ!
Tyro Hunter

28

จาก django.test.simple

  warnings.warn(
      "The django.test.simple module and DjangoTestSuiteRunner are deprecated; "
      "use django.test.runner.DiscoverRunner instead.",
      RemovedInDjango18Warning)

ดังนั้นแทนที่แทนDiscoverRunnerDjangoTestSuiteRunner

 from django.test.runner import DiscoverRunner

 class NoDbTestRunner(DiscoverRunner):
   """ A test runner to test without database creation/deletion """

   def setup_databases(self, **kwargs):
     pass

   def teardown_databases(self, old_config, **kwargs):
     pass

ใช้แบบนั้น:

python manage.py test app --testrunner=app.filename.NoDbTestRunner

8

ฉันเลือกที่จะสืบทอดจากdjango.test.runner.DiscoverRunnerและเพิ่มสองสามrun_testsวิธีในวิธีนี้

การเพิ่มครั้งแรกของฉันตรวจสอบเพื่อดูว่าจำเป็นต้องตั้งค่าฐานข้อมูลหรือไม่และอนุญาตให้setup_databasesฟังก์ชันปกติเริ่มทำงานได้หากจำเป็นต้องใช้ฐานข้อมูล การเพิ่มครั้งที่สองของฉันอนุญาตให้ทำงานปกติteardown_databasesหากsetup_databasesวิธีการได้รับอนุญาตให้ทำงาน

รหัสของฉันถือว่า TestCase ใด ๆ ที่สืบทอดมาจากdjango.test.TransactionTestCase(และดังนั้นdjango.test.TestCase) ต้องมีการตั้งค่าฐานข้อมูล ฉันตั้งสมมติฐานนี้เพราะเอกสาร Django บอกว่า:

หากคุณต้องการคุณสมบัติเฉพาะของ Django ที่ซับซ้อนและมีน้ำหนักมากเช่น ... การทดสอบหรือใช้ ORM ... คุณควรใช้ TransactionTestCase หรือ TestCase แทน

https://docs.djangoproject.com/en/1.6/topics/testing/tools/#django.test.SimpleTestCase

mysite / scripts / settings.py

from django.test import TransactionTestCase     
from django.test.runner import DiscoverRunner


class MyDiscoverRunner(DiscoverRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):
        """
        Run the unit tests for all the test labels in the provided list.

        Test labels should be dotted Python paths to test modules, test
        classes, or test methods.

        A list of 'extra' tests may also be provided; these tests
        will be added to the test suite.

        If any of the tests in the test suite inherit from
        ``django.test.TransactionTestCase``, databases will be setup. 
        Otherwise, databases will not be set up.

        Returns the number of tests that failed.
        """
        self.setup_test_environment()
        suite = self.build_suite(test_labels, extra_tests)
        # ----------------- First Addition --------------
        need_databases = any(isinstance(test_case, TransactionTestCase) 
                             for test_case in suite)
        old_config = None
        if need_databases:
        # --------------- End First Addition ------------
            old_config = self.setup_databases()
        result = self.run_suite(suite)
        # ----------------- Second Addition -------------
        if need_databases:
        # --------------- End Second Addition -----------
            self.teardown_databases(old_config)
        self.teardown_test_environment()
        return self.suite_result(suite, result)

สุดท้ายฉันได้เพิ่มบรรทัดต่อไปนี้ในไฟล์ settings.py ของโปรเจ็กต์

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

ตอนนี้เมื่อเรียกใช้เฉพาะการทดสอบที่ไม่ขึ้นกับฐานข้อมูลชุดทดสอบของฉันจะทำงานตามลำดับขนาดได้เร็วขึ้น! :)


6

อัปเดต:ยังเห็นคำตอบนี้pytestสำหรับการใช้เครื่องมือของบุคคลที่สาม


@ ซีซาร์พูดถูก หลังจากทำงาน./manage.py test --settings=no_db_settingsโดยไม่ได้ตั้งใจโดยไม่ได้ระบุชื่อแอปฐานข้อมูลการพัฒนาของฉันก็ถูกล้างออก

เพื่อความปลอดภัยยิ่งขึ้นให้ใช้แบบเดียวกันNoDbTestRunnerแต่ใช้ร่วมกับสิ่งต่อไปนี้mysite/no_db_settings.py:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

# Use an alternative database as a safeguard against accidents
DATABASES['default']['NAME'] = '_test_mysite_db'

คุณต้องสร้างฐานข้อมูลที่เรียกว่า_test_mysite_dbโดยใช้เครื่องมือฐานข้อมูลภายนอก จากนั้นรันคำสั่งต่อไปนี้เพื่อสร้างตารางที่เกี่ยวข้อง:

./manage.py syncdb --settings=mysite.no_db_settings

หากคุณใช้ South ให้เรียกใช้คำสั่งต่อไปนี้:

./manage.py migrate --settings=mysite.no_db_settings

ตกลง!

ตอนนี้คุณสามารถเรียกใช้การทดสอบหน่วยได้อย่างรวดเร็ว (และปลอดภัย) โดย:

./manage.py test myapp --settings=mysite.no_db_settings

ฉันได้ทำการทดสอบโดยใช้ pytest (ด้วยปลั๊กอิน pytest-django) และ NoDbTestRunner หากคุณสร้างวัตถุโดยบังเอิญในกล่องทดสอบและคุณไม่ได้แทนที่ชื่อฐานข้อมูลวัตถุจะถูกสร้างขึ้นในฐานข้อมูลในเครื่องของคุณที่คุณตั้งค่าไว้ใน การตั้งค่า ชื่อ 'NoDbTestRunner' ควรเป็น 'NoTestDbTestRunner' เพราะจะไม่สร้างฐานข้อมูลทดสอบ แต่จะใช้ฐานข้อมูลของคุณจากการตั้งค่า
Gabriel Muj

2

เพื่อเป็นทางเลือกในการแก้ไขการตั้งค่าของคุณเพื่อให้ NoDbTestRunner "ปลอดภัย" นี่คือเวอร์ชันแก้ไขของ NoDbTestRunner ซึ่งจะปิดการเชื่อมต่อฐานข้อมูลปัจจุบันและลบข้อมูลการเชื่อมต่อออกจากการตั้งค่าและอ็อบเจ็กต์การเชื่อมต่อ ใช้ได้ผลสำหรับฉันทดสอบในสภาพแวดล้อมของคุณก่อนที่จะใช้ :)

class NoDbTestRunner(DjangoTestSuiteRunner):
    """ A test runner to test without database creation """

    def __init__(self, *args, **kwargs):
        # hide/disconnect databases to prevent tests that 
        # *do* require a database which accidentally get 
        # run from altering your data
        from django.db import connections
        from django.conf import settings
        connections.databases = settings.DATABASES = {}
        connections._connections['default'].close()
        del connections._connections['default']
        super(NoDbTestRunner,self).__init__(*args,**kwargs)

    def setup_databases(self, **kwargs):
        """ Override the database creation defined in parent class """
        pass

    def teardown_databases(self, old_config, **kwargs):
        """ Override the database teardown defined in parent class """
        pass

หมายเหตุ:หากคุณลบการเชื่อมต่อเริ่มต้นจากรายการการเชื่อมต่อคุณจะไม่สามารถใช้โมเดล Django หรือคุณสมบัติอื่น ๆ ที่ใช้ฐานข้อมูลได้ตามปกติ (เห็นได้ชัดว่าเราไม่ได้สื่อสารกับฐานข้อมูล แต่ Django จะตรวจสอบคุณสมบัติต่าง ๆ ที่ DB รองรับ) . นอกจากนี้ดูเหมือนว่าการเชื่อมต่อ _ การเชื่อมต่อไม่รองรับ__getitem__อีกต่อไป ใช้connections._connections.defaultเพื่อเข้าถึงวัตถุ
the_drow

2

อีกวิธีหนึ่งคือให้คลาสทดสอบของคุณสืบทอดมาunittest.TestCaseแทนที่จะเป็นคลาสทดสอบของ Django เอกสาร Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) มีคำเตือนต่อไปนี้เกี่ยวกับสิ่งนี้:

การใช้ unittest.TestCase หลีกเลี่ยงค่าใช้จ่ายในการดำเนินการทดสอบแต่ละครั้งในธุรกรรมและล้างฐานข้อมูล แต่ถ้าการทดสอบของคุณโต้ตอบกับฐานข้อมูลพฤติกรรมของพวกเขาจะแตกต่างกันไปตามลำดับที่ผู้ทดสอบดำเนินการ สิ่งนี้สามารถนำไปสู่การทดสอบหน่วยที่ผ่านเมื่อรันแบบแยกส่วน แต่ล้มเหลวเมื่อรันในชุด

อย่างไรก็ตามหากการทดสอบของคุณไม่ใช้ฐานข้อมูลคำเตือนนี้ไม่จำเป็นต้องเกี่ยวข้องกับคุณและคุณสามารถเก็บเกี่ยวผลประโยชน์จากการไม่ต้องรันแต่ละกรณีทดสอบในธุรกรรม


ดูเหมือนว่าสิ่งนี้จะยังคงสร้างและทำลายฐานข้อมูลความแตกต่างเพียงอย่างเดียวคือมันไม่ได้ทำการทดสอบในธุรกรรมและไม่ได้ล้างฐานข้อมูล
Cam Rail

0

วิธีแก้ปัญหาข้างต้นก็ใช้ได้เช่นกัน แต่วิธีแก้ปัญหาต่อไปนี้จะช่วยลดเวลาในการสร้างฐานข้อมูลหากมีจำนวนการย้ายข้อมูลมากขึ้น ในระหว่างการทดสอบหน่วยการเรียกใช้ syncdb แทนที่จะเรียกใช้การย้ายข้อมูลทางทิศใต้ทั้งหมดจะเร็วกว่ามาก

SOUTH_TESTS_MIGRATE = False # เพื่อปิดใช้งานการย้ายข้อมูลและใช้ syncdb แทน


0

เว็บโฮสต์ของฉันเพียง แต่ช่วยให้การสร้างและวางฐานข้อมูลจากเว็บ GUI ของพวกเขาดังนั้นผมได้รับเป็น "มีข้อผิดพลาดในการสร้างฐานข้อมูลการทดสอบการอนุญาตปฏิเสธข้อผิดพลาด" python manage.py testเมื่อพยายามที่จะวิ่ง

ฉันหวังว่าจะใช้ตัวเลือก --keepdb เพื่อ django-admin.py แต่ดูเหมือนว่าจะไม่ได้รับการสนับสนุนอีกต่อไปตั้งแต่ Django 1.7

สิ่งที่ฉันทำคือแก้ไขโค้ด Django ใน ... / django / db / backends / creation.py โดยเฉพาะฟังก์ชัน _create_test_db และ _destroy_test_db

สำหรับ_create_test_dbฉันแสดงความคิดเห็นในcursor.execute("CREATE DATABASE ...บรรทัดและแทนที่ด้วยpassดังนั้นtryบล็อกจะไม่ว่างเปล่า

สำหรับ_destroy_test_dbฉันเพิ่งแสดงความคิดเห็นcursor.execute("DROP DATABASE- ฉันไม่จำเป็นต้องแทนที่ด้วยอะไรเพราะมีคำสั่งอื่นอยู่แล้วในบล็อก ( time.sleep(1))

หลังจากนั้นการทดสอบของฉันก็ดำเนินไปด้วยดี - แม้ว่าฉันจะตั้งค่าเวอร์ชัน test_ ของฐานข้อมูลปกติของฉันแยกต่างหาก

นี่ไม่ใช่วิธีแก้ปัญหาที่ยอดเยี่ยมแน่นอนเพราะมันจะพังหาก Django ได้รับการอัพเกรด แต่ฉันมีสำเนาของ Django ในเครื่องเนื่องจากใช้ Virtualenv ดังนั้นอย่างน้อยฉันก็สามารถควบคุมได้ว่าจะอัปเกรดเป็นเวอร์ชันที่ใหม่กว่าเมื่อใด / หาก


0

วิธีแก้ปัญหาอื่นที่ไม่ได้กล่าวถึง: นี่เป็นเรื่องง่ายสำหรับฉันที่จะนำไปใช้เพราะฉันมีไฟล์การตั้งค่าหลายไฟล์ (สำหรับโลคัล / การจัดเตรียม / การผลิต) ที่สืบทอดมาจาก base.py ดังนั้นไม่เหมือนกับคนอื่น ๆ ที่ฉันไม่ต้องเขียนทับ DATABASES ['default'] เนื่องจาก DATABASES ไม่ได้ตั้งค่าใน base.py

SimpleTestCase ยังคงพยายามเชื่อมต่อกับฐานข้อมูลทดสอบของฉันและเรียกใช้การย้ายข้อมูล เมื่อฉันสร้างไฟล์ config / settings / test.py ที่ไม่ได้ตั้งค่า DATABASES เป็นอะไรเลยการทดสอบหน่วยของฉันก็ทำงานโดยไม่มีมัน อนุญาตให้ฉันใช้โมเดลที่มีคีย์ต่างประเทศและฟิลด์ข้อ จำกัด เฉพาะ (ย้อนกลับการค้นหาคีย์ต่างประเทศซึ่งต้องใช้การค้นหาฐานข้อมูลล้มเหลว)

(Django 2.0.6)

ตัวอย่างโค้ด PS

PROJECT_ROOT_DIR/config/settings/test.py:
from .base import *
#other test settings

#DATABASES = {
# 'default': {
#   'ENGINE': 'django.db.backends.sqlite3',
#   'NAME': 'PROJECT_ROOT_DIR/db.sqlite3',
# }
#}

cli, run from PROJECT_ROOT_DIR:
./manage.py test path.to.app.test --settings config.settings.test

path/to/app/test.py:
from django.test import SimpleTestCase
from .models import *
#^assume models.py imports User and defines Classified and UpgradePrice

class TestCaseWorkingTest(SimpleTestCase):
  def test_case_working(self):
    self.assertTrue(True)
  def test_models_ok(self):
    obj = UpgradePrice(title='test',price=1.00)
    self.assertEqual(obj.title,'test')
  def test_more_complex_model(self):
    user = User(username='testuser',email='hi@hey.com')
    self.assertEqual(user.username,'testuser')
  def test_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    self.assertEqual(ad.user.username,'testuser')
  #fails with error:
  def test_reverse_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    print(user.classified_set.first())
    self.assertTrue(True) #throws exception and never gets here

0

เมื่อใช้นักวิ่งทดสอบจมูก (django-nose) คุณสามารถทำสิ่งนี้ได้:

my_project/lib/nodb_test_runner.py:

from django_nose import NoseTestSuiteRunner


class NoDbTestRunner(NoseTestSuiteRunner):
    """
    A test runner to test without database creation/deletion
    Used for integration tests
    """
    def setup_databases(self, **kwargs):
        pass

    def teardown_databases(self, old_config, **kwargs):
        pass

ในของsettings.pyคุณคุณสามารถระบุนักวิ่งทดสอบได้ที่นั่นเช่น

TEST_RUNNER = 'lib.nodb_test_runner.NoDbTestRunner' . # Was 'django_nose.NoseTestSuiteRunner'

หรือ

ฉันต้องการให้ทำการทดสอบเฉพาะเท่านั้นดังนั้นฉันจึงเรียกใช้ดังนี้:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.