Skip to content

Commit

Permalink
chore: unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
marlonkeating committed Sep 11, 2023
1 parent 3407152 commit 2cdb732
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 25 deletions.
1 change: 0 additions & 1 deletion edx_arch_experiments/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__all__ = ['ManufactureDataCommand']
14 changes: 2 additions & 12 deletions edx_arch_experiments/management/commands/manufacture_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,7 @@
from django.db import connections
from factory.declarations import SubFactory

# We have to import the enterprise test factories to ensure it's loaded and found by __subclasses__
# To ensure factories outside of the enterprise package are loaded and found by the script,
# add any additionally desired factories as an import to this file. Make sure to catch the ImportError
# incase other consumers of the command do not have the same factories installed.
# For example:
# try:
# import common.djangoapps.student.tests.factories # pylint: disable=unused-import

# from test_utils import factories # pylint: disable=unused-import
# except ImportError:
# pass
# TODO: Document usage

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -216,7 +206,7 @@ def build_tree_from_field_list(list_of_fields, provided_factory, base_node, cust
return base_node


class ManufactureDataCommand(BaseCommand):
class Command(BaseCommand):
"""
Management command for generating Django records from factories with custom attributes
Expand Down
Empty file.
Empty file.
239 changes: 239 additions & 0 deletions edx_arch_experiments/tests/management/test_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""
Test management commands and related functions.
"""

from argparse import _AppendConstAction, _CountAction, _StoreConstAction, _SubParsersAction
from pytest import mark

from django.test.utils import isolate_apps
from django.core.management import get_commands, load_command_class
from django.core.management.base import BaseCommand, CommandError
from django.test import TestCase

import factory
from django.db import models


# Copied from django.core.management.__init__.py
# https://github.com/django/django/blob/1ad7761ee616341295f36c80f78b86ff79d5b513/django/core/management/__init__.py#L83
def call_command(command_name, *args, **options):
"""
Call the given command, with the given options and args/kwargs.
This is the primary API you should use for calling specific commands.
`command_name` may be a string or a command object. Using a string is
preferred unless the command object is required for further processing or
testing.
Some examples:
call_command('migrate')
call_command('shell', plain=True)
call_command('sqlmigrate', 'myapp')
from django.core.management.commands import flush
cmd = flush.Command()
call_command(cmd, verbosity=0, interactive=False)
# Do something with cmd ...
"""
if isinstance(command_name, BaseCommand):
# Command object passed in.
command = command_name
command_name = command.__class__.__module__.split(".")[-1]
else:
# Load the command object by name.
try:
app_name = get_commands()[command_name]
except KeyError:
raise CommandError("Unknown command: %r" % command_name) # pylint: disable=raise-missing-from

if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
command = app_name
else:
command = load_command_class(app_name, command_name)

# Simulate argument parsing to get the option defaults (see #10080 for details).
parser = command.create_parser("", command_name)
# Use the `dest` option name from the parser option
opt_mapping = {
min(s_opt.option_strings).lstrip("-").replace("-", "_"): s_opt.dest
for s_opt in parser._actions # pylint: disable=protected-access
if s_opt.option_strings
}
arg_options = {opt_mapping.get(key, key): value for key, value in options.items()}
parse_args = []
for arg in args:
if isinstance(arg, (list, tuple)):
parse_args += map(str, arg)
else:
parse_args.append(str(arg))

def get_actions(parser):
# Parser actions and actions from sub-parser choices.
for opt in parser._actions: # pylint: disable=protected-access
if isinstance(opt, _SubParsersAction):
for sub_opt in opt.choices.values():
yield from get_actions(sub_opt)
else:
yield opt

parser_actions = list(get_actions(parser))
mutually_exclusive_required_options = {
opt
for group in parser._mutually_exclusive_groups # pylint: disable=protected-access
for opt in group._group_actions # pylint: disable=protected-access
if group.required
}
# Any required arguments which are passed in via **options must be passed
# to parse_args().
for opt in parser_actions:
if opt.dest in options and (
opt.required or opt in mutually_exclusive_required_options
):
opt_dest_count = sum(v == opt.dest for v in opt_mapping.values())
if opt_dest_count > 1:
raise TypeError(
f"Cannot pass the dest {opt.dest!r} that matches multiple "
f"arguments via **options."
)
parse_args.append(min(opt.option_strings))
if isinstance(opt, (_AppendConstAction, _CountAction, _StoreConstAction)):
continue
value = arg_options[opt.dest]
if isinstance(value, (list, tuple)):
parse_args += map(str, value)
else:
parse_args.append(str(value))
defaults = parser.parse_args(args=parse_args)

defaults = dict(defaults._get_kwargs(), **arg_options) # pylint: disable=protected-access
# Commented out section allows for unknown options to be passed to the command

# Raise an error if any unknown options were passed.
# stealth_options = set(command.base_stealth_options + command.stealth_options)
# dest_parameters = {action.dest for action in parser_actions}
# valid_options = (dest_parameters | stealth_options).union(opt_mapping)
# unknown_options = set(options) - valid_options
# if unknown_options:
# raise TypeError(
# "Unknown option(s) for %s command: %s. "
# "Valid options are: %s."
# % (
# command_name,
# ", ".join(sorted(unknown_options)),
# ", ".join(sorted(valid_options)),
# )
# )
# Move positional args out of options to mimic legacy optparse
args = defaults.pop("args", ())
if "skip_checks" not in options:
defaults["skip_checks"] = True

return command.execute(*args, **defaults)


@mark.django_db
class ManufactureDataCommandTests(TestCase):
"""
Test command `manufacture_data`.
"""
command = 'manufacture_data'

def test_command_requires_model(self):
"""
Test that the manufacture_data command will raise an error if no model is provided.
"""
with self.assertRaises(CommandError):
call_command(self.command)

def test_command_requires_valid_model(self):
"""
Test that the manufacture_data command will raise an error if the provided model is invalid.
"""
with self.assertRaises(CommandError):
call_command(self.command, model='FakeModel')

@isolate_apps("edx_arch_experiments")
def test_model_definition(self):
class TestPerson(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)


class TestPersonContactInfo(models.Model):
person = models.ForeignKey(TestPerson, on_delete=models.CASCADE)
address = models.CharField(max_length=100)


class TestPersonUserFactory(factory.django.DjangoModelFactory):
"""
Test Factory for TestPerson
"""
class Meta:
model = TestPerson

first_name = 'John'
last_name = 'Doe'


class TestPersonContactInfoFactory(factory.django.DjangoModelFactory):
"""
Test Factory for TestPersonContactInfo
"""
class Meta:
model = TestPersonContactInfo

address = '123 4th st, Fiveville, AZ, 67890'
command = 'manufacture_data'

# def test_single_object_create_no_customizations(self):
"""
Test that the manufacture_data command will create a single object with no customizations.
"""
assert TestPerson.objects.all().count() == 0
created_object = call_command(self.command, model='TestPerson')
assert TestPerson.objects.all().count() == 1
assert TestPerson.objects.filter(pk=created_object).exists()

# def test_command_requires_valid_field(self):
"""
Test that the manufacture_data command will raise an error if the provided field is invalid.
"""
with self.assertRaises(CommandError):
call_command(
self.command,
model='TestPerson',
field_customizations={"fake_field": 'fake_value'}
)

# def test_command_can_customize_fields(self):
"""
Test that the manufacture_data command will create a single object with customizations.
"""
assert TestPerson.objects.all().count() == 0
created_object = call_command(
self.command,
model='TestPerson',
field_customizations={'first_name': 'Steve'},
)
assert TestPerson.objects.all().count() == 1
assert TestPerson.objects.filter(pk=created_object).exists()
assert TestPerson.objects.filter(pk=created_object).first().first_name == 'Steve'

# def test_command_can_customize_nested_objects(self):
"""
Test that the manufacture_data command supports customizing nested objects.
"""
assert TestPerson.objects.all().count() == 0
assert TestPersonContactInfo.objects.all().count() == 0
created_object = call_command(
self.command,
model='TestPersonContactInfo',
field_customizations={'person__last_name': 'Nowhere'},
)
assert TestPerson.objects.all().count() == 1
assert TestPersonContactInfo.objects.all().count() == 1
assert TestPersonContactInfo.objects.filter(
pk=created_object
).first().person.last_name == 'Nowhere'
18 changes: 16 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ bleach==6.0.0
# via
# -r requirements/quality.txt
# readme-renderer
build==1.0.0
build==1.0.3
# via
# -r requirements/pip-tools.txt
# pip-tools
Expand Down Expand Up @@ -61,6 +61,8 @@ cryptography==41.0.3
# via
# -r requirements/quality.txt
# secretstorage
ddt==1.3.1
# via -r requirements/quality.txt
diff-cover==7.7.0
# via -r requirements/dev.in
dill==0.3.7
Expand Down Expand Up @@ -101,6 +103,12 @@ exceptiongroup==1.1.3
# via
# -r requirements/quality.txt
# pytest
factory-boy==3.3.0
# via -r requirements/quality.txt
faker==19.4.0
# via
# -r requirements/quality.txt
# factory-boy
filelock==3.12.3
# via
# -r requirements/ci.txt
Expand Down Expand Up @@ -260,7 +268,7 @@ pyproject-hooks==1.0.0
# via
# -r requirements/pip-tools.txt
# build
pytest==7.4.1
pytest==7.4.2
# via
# -r requirements/quality.txt
# pytest-cov
Expand All @@ -269,6 +277,10 @@ pytest-cov==4.1.0
# via -r requirements/quality.txt
pytest-django==4.5.2
# via -r requirements/quality.txt
python-dateutil==2.8.2
# via
# -r requirements/quality.txt
# faker
python-slugify==8.0.1
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -313,6 +325,7 @@ six==1.16.0
# -r requirements/quality.txt
# bleach
# edx-lint
# python-dateutil
# tox
snowballstemmer==2.2.0
# via
Expand Down Expand Up @@ -362,6 +375,7 @@ typing-extensions==4.7.1
# -r requirements/quality.txt
# asgiref
# astroid
# faker
# filelock
# pylint
# rich
Expand Down
19 changes: 14 additions & 5 deletions requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ coverage[toml]==7.3.1
# via
# -r requirements/test.txt
# pytest-cov
ddt==1.3.1
# via -r requirements/test.txt
django==3.2.21
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
Expand Down Expand Up @@ -65,9 +67,13 @@ exceptiongroup==1.1.3
# -r requirements/test.txt
# pytest
factory-boy==3.3.0
# via -r requirements/doc.in
faker==19.3.1
# via factory-boy
# via
# -r requirements/doc.in
# -r requirements/test.txt
faker==19.4.0
# via
# -r requirements/test.txt
# factory-boy
idna==3.4
# via requests
imagesize==1.4.1
Expand Down Expand Up @@ -121,7 +127,7 @@ pynacl==1.5.0
# via
# -r requirements/test.txt
# edx-django-utils
pytest==7.4.1
pytest==7.4.2
# via
# -r requirements/doc.in
# -r requirements/test.txt
Expand All @@ -132,7 +138,9 @@ pytest-cov==4.1.0
pytest-django==4.5.2
# via -r requirements/test.txt
python-dateutil==2.8.2
# via faker
# via
# -r requirements/test.txt
# faker
python-slugify==8.0.1
# via
# -r requirements/test.txt
Expand All @@ -154,6 +162,7 @@ restructuredtext-lint==1.4.0
# via doc8
six==1.16.0
# via
# -r requirements/test.txt
# bleach
# edx-sphinx-theme
# python-dateutil
Expand Down
Loading

0 comments on commit 2cdb732

Please sign in to comment.