Skip to content

Commit

Permalink
Merge pull request #151 from Mosquito-Alert/refactor_tests
Browse files Browse the repository at this point in the history
Refactor tests
  • Loading branch information
epou authored Oct 13, 2023
2 parents b36f6ba + 3714059 commit 9ec90ab
Show file tree
Hide file tree
Showing 16 changed files with 629 additions and 446 deletions.
40 changes: 19 additions & 21 deletions mosquito_alert/bites/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import pytest
from django.db.models.deletion import ProtectedError
from django.db.utils import IntegrityError
from django.db import models
from django.utils import timezone

from mosquito_alert.individuals.tests.factories import IndividualFactory
from mosquito_alert.utils.tests.test_models import AbstractDjangoModelTestMixin

from ..models import Bite
from .factories import BiteFactory


@pytest.mark.django_db
class TestBiteModel:
def test_bite_is_protected_on_individual_delete(self):
individual = IndividualFactory()
_ = BiteFactory(individual=individual)
class TestBiteModel(AbstractDjangoModelTestMixin):
model = Bite
factory_cls = BiteFactory

with pytest.raises(ProtectedError):
individual.delete()
# fields
def test_individual_can_be_null(self):
assert self.model._meta.get_field("individual").null

def test_bite_allow_null_individual(self):
_ = BiteFactory(individual=None)
def test_individual_deletion_is_protected(self):
_on_delete = self.model._meta.get_field("individual").remote_field.on_delete
assert _on_delete == models.PROTECT

def test_do_not_allow_null_bodypart(self):
with pytest.raises(IntegrityError, match=r"not-null constraint"):
_ = BiteFactory(body_part=None)
def test_datetime_auto_now_add(self):
assert self.model._meta.get_field("datetime").auto_now_add

def test_auto_datetime(self, freezer):
# freezer is fixture from pytest-freezegun
bite = BiteFactory()
assert bite.datetime == timezone.now()
def test_body_part_can_not_be_null(self):
assert not self.model._meta.get_field("body_part").null

def test__str__(self, freezer):
# freezer is fixture from pytest-freezegun
bite = BiteFactory(body_part=Bite.BodyParts.HEAD)
# meta
@pytest.mark.freeze_time
def test__str__(self):
bite = self.factory_cls(body_part=Bite.BodyParts.HEAD)
expected_str = "{} ({})".format(Bite.BodyParts.HEAD.label, timezone.now().strftime("%Y-%m-%d %H:%M:%S"))
assert bite.__str__() == expected_str
17 changes: 14 additions & 3 deletions mosquito_alert/breeding_sites/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import pytest

from mosquito_alert.utils.tests.test_models import AbstractDjangoModelTestMixin

from ..models import BreedingSite
from .factories import BreedingSiteFactory


@pytest.mark.django_db
class TestBreedingSiteModel:
def test_allow_null_type(self):
_ = BreedingSiteFactory(type=None)
class TestBreedingSiteModel(AbstractDjangoModelTestMixin):
model = BreedingSite
factory_cls = BreedingSiteFactory

# fields
def test_type_can_be_null(self):
assert self.model._meta.get_field("type").null

def test_type_can_be_blank(self):
assert self.model._meta.get_field("type").blank

# meta
def test__str__with_location_type(self):
obj = BreedingSiteFactory()
loc = obj.location
Expand Down
7 changes: 7 additions & 0 deletions mosquito_alert/epidemiology/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,12 @@ def diseases(self, create, extracted, **kwargs):

self.diseases.add(*extracted)

@classmethod
def _after_postgeneration(cls, instance, create, results=None):
# diseases is already set. Do not call obj.save againg
if results:
_ = results.pop("diseases", None)
super()._after_postgeneration(instance=instance, create=create, results=results)

class Meta:
model = DiseaseVector
95 changes: 40 additions & 55 deletions mosquito_alert/epidemiology/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,68 @@
import pytest
from django.db.utils import DataError, IntegrityError
from django.db import models

from mosquito_alert.taxa.models import Taxon
from mosquito_alert.taxa.tests.factories import SpecieDistributionFactory, TaxonFactory
from mosquito_alert.utils.tests.test_models import AbstractDjangoModelTestMixin

from ..models import Disease, DiseaseVector, DiseaseVectorDistribution
from .factories import DiseaseFactory, DiseaseVectorFactory


@pytest.mark.django_db
class TestDiseaseModel:
def test_default_order_by_name(self):
d1 = DiseaseFactory(name="Z")
d2 = DiseaseFactory(name="A")

assert frozenset(Disease.objects.all()) == frozenset([d2, d1])

def test_name_must_be_unique(self):
with pytest.raises(IntegrityError, match=r"unique constraint"):
_ = DiseaseFactory.create_batch(size=2, name="Unique Name")

@pytest.mark.parametrize(
"name, output_raises",
[
("a" * 63, False),
("a" * 64, False),
("a" * 65, True),
],
)
def test_name_max_length_is_64(self, name, output_raises):
if output_raises:
with pytest.raises(DataError):
DiseaseFactory(name=name)
else:
DiseaseFactory(name=name)
class TestDiseaseModel(AbstractDjangoModelTestMixin):
model = Disease
factory_cls = DiseaseFactory

# fields
def test_name_is_unique(self):
assert self.model._meta.get_field("name").unique

def test_name_max_length_is_64(self):
assert self.model._meta.get_field("name").max_length == 64

# meta
def test_ordering_ascending_name(self):
assert self.model._meta.ordering == ["name"]

def test__str__(self):
disease = DiseaseFactory(name="Malaria")
assert disease.__str__() == "Malaria"


@pytest.mark.django_db
class TestDiseaseVectorModel:
def test_diseasse_vector_one2one_taxon(self, taxon_specie):
with pytest.raises(IntegrityError, match=r"unique constraint"):
_ = DiseaseVectorFactory.create_batch(size=2, taxon=taxon_specie)
class TestDiseaseVectorModel(AbstractDjangoModelTestMixin):
model = DiseaseVector
factory_cls = DiseaseVectorFactory

def test_disease_vector_pk_is_taxon(self, taxon_specie):
dv = DiseaseVectorFactory(taxon=taxon_specie)
# fields
def test_taxon_fk_is_unique(self):
assert self.model._meta.get_field("taxon").unique

assert dv.pk == taxon_specie.pk
def test_taxon_is_pk(self):
assert self.model._meta.get_field("taxon").primary_key

def test_cascade_taxon_deletion(self):
dv = DiseaseVectorFactory()
dv.taxon.delete()
def test_taxon_deletion_is_cascaded(self):
_on_delete = self.model._meta.get_field("taxon").remote_field.on_delete
assert _on_delete == models.CASCADE

assert DiseaseVector.objects.all().count() == 0
def test_taxon_related_name(self):
assert self.model._meta.get_field("taxon").remote_field.related_name == "disease_vector"

def test_taxon_can_not_be_null(self):
with pytest.raises(IntegrityError, match=r"not-null constraint"):
DiseaseVectorFactory(taxon=None)
assert not self.model._meta.get_field("taxon").null

def test_taxon_related_name(self, taxon_specie):
dv = DiseaseVectorFactory(taxon=taxon_specie)

assert taxon_specie.disease_vector == dv
def test_diseases_related_name(self):
assert self.model._meta.get_field("diseases").remote_field.related_name == "disease_vectors"

# methods
def test_taxon_should_be_species_rank(self):
with pytest.raises(ValueError, match=r"Taxon must be species rank."):
DiseaseVectorFactory(taxon__rank=Taxon.TaxonomicRank.CLASS)

def test_default_ordering_is_taxon_name(self, taxon_root):
dv1 = DiseaseVectorFactory(taxon__name="z", taxon__parent=taxon_root)
dv2 = DiseaseVectorFactory(taxon__name="a", taxon__parent=taxon_root)

assert list(DiseaseVector.objects.all()) == [dv2, dv1]

def test_diseases_related_name(self):
disease = DiseaseFactory()
dv = DiseaseVectorFactory(diseases=[disease])

assert list(disease.disease_vectors.all()) == [dv]
# meta
def test_default_ordering_is_taxon_name_asc(self, taxon_root):
assert self.model._meta.ordering == ["taxon__name"]

def test__str__(self):
dv = DiseaseVectorFactory(taxon__name="Random name")
Expand All @@ -89,7 +71,10 @@ def test__str__(self):


@pytest.mark.django_db
class TestDiseaseVectorDistribution:
class TestDiseaseVectorDistribution(AbstractDjangoModelTestMixin):
model = DiseaseVectorDistribution
factory_cls = None

def test_should_filter_by_taxon_vectors(self, taxon_root, country_bl):
taxon_not_vector = TaxonFactory(parent=taxon_root, rank=Taxon.TaxonomicRank.SPECIES)
disease_vector = DiseaseVectorFactory(taxon__parent=taxon_root, taxon__name="test")
Expand Down
77 changes: 52 additions & 25 deletions mosquito_alert/geo/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.gis import geos
from django.contrib.gis.db import models
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -77,36 +78,62 @@ def update_descendants_boundaries(self):
# will not be called for these cases.
self.get_descendants().update(boundary=self.boundary)

def save(self, *args, **kwargs):
def clean_boundary_type_field(self):
try:
if not self.is_root() and (root := self.get_root()):
if self.boundary_type != root.boundary_type:
raise ValueError(f"Only {root.boundary_type} boundary layer nodes are allowed in {root} tree.")
raise ValidationError(
f"Only {root.boundary_type} boundary layer nodes are allowed in {root} tree."
)
except self.__class__.DoesNotExist:
# self.get_root() has not found any result.
pass

def clean_level_field(self):
if not self.parent:
return

# Checking that the current level is smaller than its parent
if self.level <= self.parent.level:
raise ValidationError("Level must be ascending order from parent to children.")

def _clean_custom_fields(self, exclude=None) -> None:
if exclude is None:
exclude = []

errors = {}
if "boundary_type" not in exclude:
try:
self.clean_boundary_type_field()
except ValidationError as e:
errors["boundary_type"] = e.error_list

if "level" not in exclude:
try:
self.clean_level_field()
except ValidationError as e:
errors["level"] = e.error_list

if errors:
raise ValidationError(errors)

def clean_fields(self, exclude=None) -> None:
super().clean_fields(exclude=exclude)
self._clean_custom_fields(exclude=exclude)

def save(self, *args, **kwargs):
# Using depth as level default value
if self._state.adding:
# If creating
if self.parent:
# Getting from parent + 1
self.level = self.parent.level + 1
else:
# Getting from node depth.
# Substracting 1 since root depth starts from 1, while level start from 0.
self.level = 0
else:
# If updating object
if self.parent:
# Checking that the current level is smaller than its parent
if self.level <= self.parent.level:
raise ValueError("Level must be ascending order from parent to children.")

# Inherit the boundary owner from the parent.
if self.parent and (p_boundary := self.parent.boundary):
# NOTE: If someday we need to on self.boundary update, change the below update() method.
self.boundary = p_boundary
if self.level is None:
self.level = self.parent.level + 1 if self.parent else 0

if self.boundary is None:
# Inherit the boundary owner from the parent.
if self.parent and (p_boundary := self.parent.boundary):
# NOTE: If someday we need to on self.boundary update, change the below update() method.
self.boundary = p_boundary

self._clean_custom_fields()

super().save(*args, **kwargs)

Expand Down Expand Up @@ -146,14 +173,14 @@ class Boundary(ParentManageableNodeMixin, TimeStampedModel, MP_Node):
# Custom Properties
node_order_by = ["name"] # Needed for django-treebeard

@cached_property
def geometry(self):
return self.get_geometry()

@property
def boundary_type(self):
return self.boundary_layer.boundary_type

@cached_property
def geometry(self):
return self.get_geometry()

# Methods
def get_geometry(self):
try:
Expand Down
8 changes: 7 additions & 1 deletion mosquito_alert/geo/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class BoundaryLayerFactory(DjangoModelFactory):
)
boundary_type = factory.Faker("random_element", elements=BoundaryLayer.BoundaryType.values)
name = factory.Faker("name")
level = factory.Faker("random_int", min=0, max=5)
description = factory.Faker("paragraph")

class Meta:
Expand Down Expand Up @@ -61,6 +60,13 @@ def boundaries(self, create, extracted, **kwargs):

self.boundaries.add(*extracted)

@classmethod
def _after_postgeneration(cls, instance, create, results=None):
# boundaries is already set. Do not call obj.save againg
if results:
_ = results.pop("boundaries", None)
super()._after_postgeneration(instance=instance, create=create, results=results)

class Meta:
model = Location

Expand Down
4 changes: 2 additions & 2 deletions mosquito_alert/geo/tests/fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class FuzzyPolygon(factory.fuzzy.BaseFuzzyAttribute):

def __init__(self, srid=None, length=None, **kwargs):
if length is None:
length = random.randgen.randrange(3, 20, 1)
length = random.randgen.randrange(3, 6, 1)
if length < 3:
raise Exception("Polygon needs to be 3 or greater in length.")
self.length = length
Expand All @@ -39,7 +39,7 @@ class FuzzyMultiPolygon(factory.fuzzy.BaseFuzzyAttribute):

def __init__(self, srid=None, length=None, **kwargs):
if length is None:
length = random.randgen.randrange(2, 20, 1)
length = random.randgen.randrange(2, 5, 1)
if length < 2:
raise Exception("MultiPolygon needs to be 2 or greater in length.")
self.length = length
Expand Down
Loading

0 comments on commit 9ec90ab

Please sign in to comment.