Skip to content
This repository has been archived by the owner on Oct 3, 2019. It is now read-only.

Commit

Permalink
Split up test structure and add more unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jacebrowning committed May 17, 2015
1 parent 4f78b3f commit 974aaac
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 59 deletions.
41 changes: 35 additions & 6 deletions Makefile
Expand Up @@ -12,17 +12,24 @@ endif

# Test settings
UNIT_TEST_COVERAGE := 98
INTEGRATION_TEST_COVERAGE := 100
INTEGRATION_TEST_COVERAGE := 87
COMBINED_TEST_COVERAGE := 100

# System paths
PLATFORM := $(shell python -c 'import sys; print(sys.platform)')
ifneq ($(findstring win32, $(PLATFORM)), )
WINDOWS := 1
SYS_PYTHON_DIR := C:\\Python$(PYTHON_MAJOR)$(PYTHON_MINOR)
SYS_PYTHON := $(SYS_PYTHON_DIR)\\python.exe
SYS_VIRTUALENV := $(SYS_PYTHON_DIR)\\Scripts\\virtualenv.exe
# https://bugs.launchpad.net/virtualenv/+bug/449537
export TCL_LIBRARY=$(SYS_PYTHON_DIR)\\tcl\\tcl8.5
else
ifneq ($(findstring darwin, $(PLATFORM)), )
MAC := 1
else
LINUX := 1
endif
SYS_PYTHON := python$(PYTHON_MAJOR)
ifdef PYTHON_MINOR
SYS_PYTHON := $(SYS_PYTHON).$(PYTHON_MINOR)
Expand Down Expand Up @@ -58,6 +65,7 @@ PYREVERSE := $(BIN)/pyreverse
NOSE := $(BIN)/nosetests
PYTEST := $(BIN)/py.test
COVERAGE := $(BIN)/coverage
SNIFFER := $(BIN)/sniffer

# Flags for PHONY targets
DEPENDS_CI := $(ENV)/.depends-ci
Expand Down Expand Up @@ -101,7 +109,16 @@ $(DEPENDS_CI): Makefile
.PHONY: depends-dev
depends-dev: env Makefile $(DEPENDS_DEV)
$(DEPENDS_DEV): Makefile
$(PIP) install --upgrade pip pep8radius pygments docutils pdoc wheel
$(PIP) install --upgrade pip pep8radius pygments docutils pdoc wheel sniffer pync
ifdef WINDOWS
$(PIP) install --upgrade pywin32
endif
ifdef MAC
$(PIP) install --upgrade MacFSEvents
endif
ifdef LINUX
$(PIP) install --upgrade pyinotify
endif
touch $(DEPENDS_DEV) # flag to indicate dependencies are installed

# Documentation ################################################################
Expand Down Expand Up @@ -168,20 +185,32 @@ PYTEST_COV_OPTS := --cov=$(PACKAGE) --cov-report=term-missing --cov-report=html
PYTEST_CAPTURELOG_OPTS := --log-format="%(name)-26s %(funcName)-20s %(lineno)3d %(levelname)s: %(message)s"
PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_CAPTURELOG_OPTS)

.PHONY: test
.PHONY: test-unit
test-unit: test
test: depends-ci .clean-test
$(PYTEST) $(PYTEST_OPTS) $(PACKAGE)
$(COVERAGE) report --fail-under=$(UNIT_TEST_COVERAGE) > /dev/null

.PHONY: tests
tests: depends-ci .clean-test
TEST_INTEGRATION=1 $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) --exitfirst
.PHONY: test-int
test-int: depends-ci .clean-test
$(PYTEST) $(PYTEST_OPTS) tests --exitfirst
$(COVERAGE) report --fail-under=$(INTEGRATION_TEST_COVERAGE) > /dev/null

.PHONY: test-all
test-all: tests
tests: depends-ci .clean-test
$(PYTEST) $(PYTEST_OPTS) $(PACKAGE) tests --exitfirst
$(COVERAGE) report --fail-under=$(COMBINED_TEST_COVERAGE) > /dev/null

.PHONY: read-coverage
read-coverage:
$(OPEN) htmlcov/index.html

.PHONY: watch
watch: depends-dev
touch htmlcov/index.html && $(MAKE) read-coverage
$(SNIFFER)

# Cleanup ######################################################################

.PHONY: clean
Expand Down
37 changes: 37 additions & 0 deletions scent.py
@@ -0,0 +1,37 @@
import os
import subprocess

from sniffer.api import select_runnable, file_validator, runnable
from pync import Notifier


watch_paths = ['yorm/', 'tests/']


@select_runnable('python_tests')
@file_validator
def py_files(filename):
return all((filename.endswith('.py'),
not os.path.basename(filename).startswith('.')))


@runnable
def python_tests(*args):

for count, (command, title) in enumerate((
(('make', 'test-unit'), "Unit Tests"),
(('make', 'test-int'), "Integration Tests"),
(('make', 'test-all'), "Combined Tests"),
), start=1):

failure = subprocess.call(command)

if failure:
mark = "❌" * count
Notifier.notify(mark + " [FAIL] " + mark, title=title)
return False
else:
mark = "✅" * count
Notifier.notify(mark + " [PASS] " + mark, title=title)

return True
23 changes: 23 additions & 0 deletions tests/__init__.py
@@ -0,0 +1,23 @@
#!/usr/bin/env python

"""Integration tests for the `yorm` package."""

import time
import logging


def strip(text, tabs=None, end='\n'):
"""Strip leading whitespace indentation on multiline string literals."""
lines = text.strip().splitlines()
for index in range(len(lines)):
if not tabs:
tabs = lines[index].count(' ' * 4)
lines[index] = lines[index].replace(' ' * tabs * 4, '')
return '\n'.join(lines) + end


def refresh_file_modification_times(seconds=1.1):
"""Sleep to allow file modification times to refresh."""
logging.info("delaying for %s second%s...", seconds,
"" if seconds == 1 else "s")
time.sleep(seconds)
File renamed without changes.
6 changes: 0 additions & 6 deletions yorm/test/test_all.py → tests/test_all.py
Expand Up @@ -12,10 +12,7 @@
from . import strip, refresh_file_modification_times
from .samples import * # pylint: disable=W0401,W0614

integration = pytest.mark.integration


@integration
class TestStandard:

"""Integration tests for standard attribute types."""
Expand Down Expand Up @@ -162,7 +159,6 @@ def test_auto_off(self, tmpdir):
""") == sample.yorm_mapper.text


@integration
class TestContainers:

"""Integration tests for attribute containers."""
Expand Down Expand Up @@ -256,7 +252,6 @@ def test_objects(self, tmpdir):
""") == sample.yorm_mapper.text


@integration
class TestExtended:

"""Integration tests for extended attribute types."""
Expand Down Expand Up @@ -298,7 +293,6 @@ def test_function(self, tmpdir):
assert "This is a sentence." == sample.text


@integration
class TestCustom:

"""Integration tests for custom attribute types."""
Expand Down
File renamed without changes.
4 changes: 0 additions & 4 deletions yorm/test/test_all_files.py → tests/test_all_files.py
Expand Up @@ -10,10 +10,7 @@
from . import refresh_file_modification_times
from .samples import * # pylint: disable=W0401,W0614

integration = pytest.mark.integration


@integration
class TestInit:

"""Integration tests for initializing mapped classes."""
Expand Down Expand Up @@ -46,7 +43,6 @@ def test_fetch_from_existing(self, tmpdir):
assert False is sample2.false


@integration
class TestDelete:

"""Integration tests for deleting files."""
Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 2 additions & 3 deletions yorm/base/convertible.py
Expand Up @@ -4,14 +4,13 @@

from .. import common
from . import MESSAGE
from .mappable import Mappable
from .converter import Converter


log = common.logger(__name__)


class Convertible(Mappable, Converter):
class Convertible(Converter): # pragma: no cover (abstract)

"""Base class for mutable attributes."""

Expand All @@ -22,7 +21,7 @@ def to_value(cls, data):
return value

@abc.abstractmethod
def update_value(self, data): # pylint: disable=E0213
def update_value(self, data): # pylint: disable=E0213,
"""Update the attribute's value from loaded data."""
raise NotImplementedError(MESSAGE)

Expand Down
27 changes: 12 additions & 15 deletions yorm/converters/containers.py
@@ -1,13 +1,23 @@
"""Converter classes for builtin container types."""

from .. import common
from ..base.mappable import Mappable
from ..base.convertible import Convertible
from . import standard

log = common.logger(__name__)


class Dictionary(Convertible, dict):
class Container(Mappable, Convertible): # pylint: disable=W0223

"""Base class for containers of attribute converters."""

@classmethod
def create_default(cls):
return cls.__new__(cls)


class Dictionary(Container, dict):

"""Base class for a dictionary of attribute converters."""

Expand All @@ -17,14 +27,6 @@ def __new__(cls, *args, **kwargs):
raise NotImplementedError(msg)
return super().__new__(cls, *args, **kwargs)

@classmethod
def create_default(cls):
"""Create an uninitialized object."""
if cls is Dictionary:
msg = "Dictionary class must be subclassed to use"
raise NotImplementedError(msg)
return cls.__new__(cls)

@classmethod
def to_data(cls, value):
value = cls.to_value(value)
Expand Down Expand Up @@ -85,7 +87,7 @@ def update_value(self, data):
self.update(value)


class List(Convertible, list):
class List(Container, list):

"""Base class for a homogeneous list of attribute converters."""

Expand All @@ -103,11 +105,6 @@ def item_type(cls): # pylint: disable=E0213
"""Get the converter class for all items."""
return common.ATTRS[cls].get(cls.ALL)

@classmethod
def create_default(cls):
"""Create an uninitialized object."""
return cls.__new__(cls)

@classmethod
def to_data(cls, value):
value = cls.to_value(value)
Expand Down
2 changes: 1 addition & 1 deletion yorm/test/__init__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python

"""Tests for the `yorm` package."""
"""Unit tests for the `yorm` package."""

import time
import logging
Expand Down
13 changes: 0 additions & 13 deletions yorm/test/conftest.py

This file was deleted.

8 changes: 8 additions & 0 deletions yorm/test/test_base_converter.py
Expand Up @@ -16,6 +16,14 @@ def test_converter_class_cannot_be_instantiated(self):
with pytest.raises(TypeError):
Converter() # pylint: disable=E0110

def test_converter_class_methods_cannot_be_called(self):
with pytest.raises(NotImplementedError):
Converter.create_default()
with pytest.raises(NotImplementedError):
Converter.to_value(None)
with pytest.raises(NotImplementedError):
Converter.to_data(None)


if __name__ == '__main__':
pytest.main()
27 changes: 22 additions & 5 deletions yorm/test/test_base_convertible.py
Expand Up @@ -12,15 +12,32 @@ class TestConvertible:

"""Unit tests for the `Convertible` class."""

class MyConvertible(Convertible):

def __init__(self, number):
self.value = number

@classmethod
def create_default(cls):
return 1

@classmethod
def to_data(cls, value):
return str(value.value)

def update_value(self, data):
self.value += int(data)

def test_convertible_class_cannot_be_instantiated(self):
with pytest.raises(TypeError):
Convertible() # pylint: disable=E0110

def test_convertible_class_methods_cannot_be_called(self):
with pytest.raises(NotImplementedError):
Convertible.create_default()
with pytest.raises(NotImplementedError):
Convertible.update_value(None, None)
def test_convertible_instance_methods_can_be_called(self):
convertible = self.MyConvertible(42)
assert 42 == convertible.value
convertible.update_value(10)
assert 52 == convertible.value
assert "52" == convertible.format_data()


if __name__ == '__main__':
Expand Down

0 comments on commit 974aaac

Please sign in to comment.