0

I write a custom test runner in Django to add custom argument '--headless', but the side effect is i can not use some of the default argument. I am using Django 1.9.11. My test runner code is:

from django.test.runner import DiscoverRunner        
class IbesTestRunner(DiscoverRunner):
    @classmethod                                    
    def add_arguments(cls, parser):
        parser.add_argument(
            '--headless',
            action='store_true', default=False, dest='headless',
            help='This is custom optional arguments for IBES.'
            'Use this option to do browser testing without GUI')

The result of ./manage.py test -h when using this test runner is:

usage: manage.py test [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
                  [--pythonpath PYTHONPATH] [--traceback] [--no-color]
                  [--noinput] [--failfast] [--testrunner TESTRUNNER]
                  [--liveserver LIVESERVER] [--headless]
                  [test_label [test_label ...]]
. . .

While using the default test runner, the result of ./manage.py test -h is:

usage: manage.py test [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
                  [--pythonpath PYTHONPATH] [--traceback] [--no-color]
                  [--noinput] [--failfast] [--testrunner TESTRUNNER]
                  [--liveserver LIVESERVER] [-t TOP_LEVEL] [-p PATTERN]
                  [-k] [-r] [-d] [--parallel [N]]
                  [test_label [test_label ...]]
...

Notice that I can not use some arguments like, -k, -p ,-r, etc. How can I add custom test arguments but not losing the default test argument?

slackmart
  • 4,754
  • 3
  • 25
  • 39
pupil
  • 318
  • 2
  • 16

1 Answers1

0

The test runner is loaded in django/core/management/commands/test.py

class Command(BaseCommand):
    help = 'Discover and run tests in the specified modules or the current directory.'
    # ... more code goes here
    def add_arguments(self, parser):
        test_runner_class = get_runner(settings, self.test_runner)

        if hasattr(test_runner_class, 'add_arguments'):
            test_runner_class.add_arguments(parser)
    # ... more code goes here

Django will append the arguments defined in your test runner but the add_arguments is a class method and the default behavior is omitted unless you explicitly execute the DiscoverRunner.add_arguments method.

So the solution is to call the add_arguments of your parent's IbesTestRunner class, something like this:

from django.test.runner import DiscoverRunner


class IbesTestRunner(DiscoverRunner):
    @classmethod                                    
    def add_arguments(cls, parser):
        parser.add_argument(
            '--headless',
            action='store_true', default=False, dest='headless',
            help='This is custom optional arguments for IBES.'
                 'Use this option to do browser testing without GUI')
        # Adding default test runner arguments.
        # Remember python takes care of passing the cls argument.
        DiscoverRunner.add_arguments(parser)
slackmart
  • 4,754
  • 3
  • 25
  • 39
  • 1
    Thanks for the answer, It worked. I think I need to study more about class in python. Is this a common python class characteristic or specific to django? And why only some argument are missing? – pupil Nov 14 '16 at 02:49
  • 1
    It's a python's feature. When defining a method and decorating it with the `classmethod` decorator, the right method definition must include a first argument (`cls` by convention), then when you need to call it explicitly, python will pass the class as the `cls` argument, so you only need to send the remaining arguments (if required by the method definition). Refer to the docs to more details https://docs.python.org/2/library/functions.html?highlight=classmethod#classmethod – slackmart Nov 15 '16 at 02:44
  • 1
    It's a common feature of all OOP programming languages. You inherited from a class that already has defined behaviour for the method you're overriding. If you want to inherit that defined behaviour in your subclass, you need to call the parent's method (typically that's done with `super`). – DylanYoung Sep 23 '22 at 03:42
  • 2
    Well, yes. I agree with @DylanYoung regarding methods being overridden and the `super` use case. However in this particular case, we are speaking about class methods, these are different from instance methods (at least in python) and cannot be invoked using `super`. This message is thrown: `AttributeError: 'super' object has no attribute 'hello'`. – slackmart Sep 27 '22 at 15:20
  • 1
    @slackmart You don't reference `hello` in any of the code here, so it's difficult to know what would cause that error. Did you try `super().add_arguments(parser)`? I don't know how `super()` interacts with `@classmethod`s. Essentially you are doing the same thing explicitly by referencing the super class by name. – Code-Apprentice Sep 27 '22 at 15:50
  • I just created a quick proof of concept. I basically created a couple of classes (let's say A and B). Class A defines a `def hello(cls, msg)` class method and B overrides `hello(cls, msg)`, then from B.hello, I called `super().hello(cls, msg)`. A.hello never got called and instead I got the error I pasted in previous comment. Quick workaround use: `A.hello(msg)`. `super` will work for instance methods through. – slackmart Sep 27 '22 at 15:56
  • 2
    @slackmart K, makes sense. I guess super() only works with instance methods, not class methods. – Code-Apprentice Sep 27 '22 at 16:12
  • 1
    Plain `super` won't work, but if you call it with explicit arguments, it will. See https://stackoverflow.com/a/1269224/3124256. – DylanYoung Oct 11 '22 at 23:56