diff --git a/{{cookiecutter.repo_name}}/.travis.yml b/{{cookiecutter.repo_name}}/.travis.yml new file mode 100644 index 00000000..f7e40a1e --- /dev/null +++ b/{{cookiecutter.repo_name}}/.travis.yml @@ -0,0 +1,16 @@ +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb + - sudo apt-get install -qq libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms1-dev libwebp-dev + - sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip + - sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev + - sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm +language: python +python: + - "3.4" +install: + - "pip install hitch" + - "cd tests" + - "hitch init" +script: + - "hitch test . --extra '{\"xvfb\":true, \"pause_on_failure\":false}'" diff --git a/{{cookiecutter.repo_name}}/README.rst b/{{cookiecutter.repo_name}}/README.rst index e6a72a27..81f5d58e 100644 --- a/{{cookiecutter.repo_name}}/README.rst +++ b/{{cookiecutter.repo_name}}/README.rst @@ -173,8 +173,32 @@ You must set the DSN url in production. It's time to write the code!!! +Running end to end integration tests +------------------------------------ + +N.B. The integration tests will not run on Windows. + +To install the test runner:: + + $ pip install hitch + +To run the tests, enter the {{cookiecutter.repo_name}}/tests directory and run the following commands:: + + $ hitch init + +Then run the stub test:: + + $ hitch test stub.test + +This will download and compile python, postgres and redis and install all python requirements so the first time it runs it may take a while. + +Subsequent test runs will be much quicker. + +The testing framework runs Django, Celery (if enabled), Postgres, HitchSMTP (a mock SMTP server), Firefox/Selenium and Redis. + + Deployment ------------- +---------- It is possible to deploy to Heroku or to your own server by using Dokku, an open source Heroku clone. diff --git a/{{cookiecutter.repo_name}}/requirements.apt b/{{cookiecutter.repo_name}}/requirements.apt index 46cfaac2..bbbaa3c9 100644 --- a/{{cookiecutter.repo_name}}/requirements.apt +++ b/{{cookiecutter.repo_name}}/requirements.apt @@ -22,3 +22,23 @@ libwebp-dev ##django-extensions graphviz-dev + +##hitch +python-setuptools +python3-dev +python-virtualenv +python-pip +firefox +automake +libtool +libreadline6 +libreadline6-dev +libreadline-dev +libsqlite3-dev +libxml2 +libxml2-dev +libssl-dev +libbz2-dev +wget +curl +llvm diff --git a/{{cookiecutter.repo_name}}/tests/base.yml b/{{cookiecutter.repo_name}}/tests/base.yml new file mode 100644 index 00000000..844f1271 --- /dev/null +++ b/{{cookiecutter.repo_name}}/tests/base.yml @@ -0,0 +1,4 @@ +{% raw %}{% for python_version in python_versions %} +{% block test scoped %} +{% endblock %} +{% endfor %}{% endraw %} diff --git a/{{cookiecutter.repo_name}}/tests/engine.py b/{{cookiecutter.repo_name}}/tests/engine.py new file mode 100644 index 00000000..388b08fd --- /dev/null +++ b/{{cookiecutter.repo_name}}/tests/engine.py @@ -0,0 +1,158 @@ +from subprocess import call +from os import path +import hitchpostgres +import hitchselenium +import hitchpython +import hitchserve +import hitchredis +import hitchtest +import hitchsmtp + + +# Get directory above this file +PROJECT_DIRECTORY = path.abspath(path.join(path.dirname(__file__), '..')) + + +class ExecutionEngine(hitchtest.ExecutionEngine): + """Engine for orchestating and interacting with the app.""" + + def set_up(self): + """Ensure virtualenv present, then run all services.""" + python_package = hitchpython.PythonPackage( + python_version=self.preconditions['python_version'] + ) + python_package.build() + python_package.verify() + + call([ + python_package.pip, "install", "-r", + path.join(PROJECT_DIRECTORY, "requirements/local.txt") + ]) + + postgres_package = hitchpostgres.PostgresPackage( + version=self.settings["postgres_version"], + ) + postgres_package.build() + postgres_package.verify() + + redis_package = hitchredis.RedisPackage(version="2.8.4") + redis_package.build() + redis_package.verify() + + self.services = hitchserve.ServiceBundle( + project_directory=PROJECT_DIRECTORY, + startup_timeout=float(self.settings["startup_timeout"]), + shutdown_timeout=5.0, + ) + + postgres_user = hitchpostgres.PostgresUser("{{cookiecutter.repo_name}}", "password") + + self.services['Postgres'] = hitchpostgres.PostgresService( + postgres_package=postgres_package, + users=[postgres_user, ], + databases=[hitchpostgres.PostgresDatabase("{{cookiecutter.repo_name}}", postgres_user), ] + ) + + self.services['HitchSMTP'] = hitchsmtp.HitchSMTPService(port=1025) + + self.services['Django'] = hitchpython.DjangoService( + python=python_package.python, + port=8000, + version=str(self.settings.get("django_version")), + settings="config.settings.local", + needs=[self.services['Postgres'], ], + env_vars=self.settings['environment_variables'], + ) + + self.services['Redis'] = hitchredis.RedisService( + redis_package=redis_package, + port=16379, + ) +{% if cookiecutter.celery_support == "y" %} + self.services['Celery'] = hitchpython.CeleryService( + python=python_package.python, + version="3.1.18", + app="{{cookiecutter.repo_name}}.taskapp", loglevel="INFO", + needs=[ + self.services['Redis'], self.services['Django'], + ], + env_vars=self.settings['environment_variables'], + ) +{% endif %} + self.services['Firefox'] = hitchselenium.SeleniumService( + xvfb=self.settings.get("quiet", False), + no_libfaketime=True, + ) + +# import hitchcron +# self.services['Cron'] = hitchcron.CronService( +# run=self.services['Django'].manage("trigger").command, +# every=1, +# needs=[ self.services['Django'], ], +# ) + + self.services.startup(interactive=False) + + # Configure selenium driver + self.driver = self.services['Firefox'].driver + self.driver.set_window_size(self.settings['window_size']['height'], self.settings['window_size']['width']) + self.driver.set_window_position(0, 0) + self.driver.implicitly_wait(2.0) + self.driver.accept_next_alert = True + + def pause(self, message=None): + """Stop. IPython time.""" + if hasattr(self, 'services'): + self.services.start_interactive_mode() + self.ipython(message) + if hasattr(self, 'services'): + self.services.stop_interactive_mode() + + def load_website(self): + """Navigate to website in Firefox.""" + self.driver.get(self.services['Django'].url()) + + def click(self, on): + """Click on HTML id.""" + self.driver.find_element_by_id(on).click() + + def fill_form(self, **kwargs): + """Fill in a form with id=value.""" + for element, text in kwargs.items(): + self.driver.find_element_by_id(element).send_keys(text) + + def click_submit(self): + """Click on a submit button if it exists.""" + self.driver.find_element_by_css_selector("button[type=\"submit\"]").click() + + def confirm_emails_sent(self, number): + """Count number of emails sent by app.""" + assert len(self.services['HitchSMTP'].logs.json()) == int(number) + + def wait_for_email(self, containing=None): + """Wait for, and return email.""" + self.services['HitchSMTP'].logs.out.tail.until_json( + lambda email: containing in email['payload'] or containing in email['subject'], + timeout=25, + lines_back=1, + ) + + def time_travel(self, days=""): + """Make all services think that time has skipped forward.""" + self.services.time_travel(days=int(days)) + + def on_failure(self): + """Stop and IPython.""" + if not self.settings['quiet']: + if self.settings.get("pause_on_failure", False): + self.pause(message=self.stacktrace.to_template()) + + def on_success(self): + """Pause on success if enabled.""" + if self.settings.get("pause_on_success", False): + self.pause(message="SUCCESS") + + def tear_down(self): + """Shut down services required to run your test.""" + if hasattr(self, 'services'): + self.services.shutdown() diff --git a/{{cookiecutter.repo_name}}/tests/hitchreqs.txt b/{{cookiecutter.repo_name}}/tests/hitchreqs.txt new file mode 100644 index 00000000..c14254a6 --- /dev/null +++ b/{{cookiecutter.repo_name}}/tests/hitchreqs.txt @@ -0,0 +1,26 @@ +click==4.0 +colorama==0.3.3 +faketime==0.9.6.3 +hitchcron==0.2 +hitchpostgres==0.6.1 +hitchpython==0.2 +hitchredis==0.4.1 +hitchselenium==0.3.1 +hitchserve==0.4.2 +hitchsmtp==0.2.1 +hitchtest==0.7 +humanize==0.5.1 +ipython==3.1.0 +Jinja2==2.7.3 +MarkupSafe==0.23 +patool==1.7 +psutil==3.0.0 +python-build==0.2.1 +pyuv==1.0.2 +PyYAML==3.11 +requests==2.7.0 +selenium==2.46.0 +six==1.9.0 +tblib==1.0.1 +tornado==4.2 +xeger==0.3 diff --git a/{{cookiecutter.repo_name}}/tests/settings.yml b/{{cookiecutter.repo_name}}/tests/settings.yml new file mode 100644 index 00000000..7ea14f07 --- /dev/null +++ b/{{cookiecutter.repo_name}}/tests/settings.yml @@ -0,0 +1,59 @@ +postgres_version: 9.3.9 +redis_version: 2.8.4 +django_version: 1.8.3 +celery_version: 3.1.18 +pause_on_success: false +pause_on_failure: true +startup_timeout: 45 +environment_variables: + DATABASE_URL: postgres://{{cookiecutter.repo_name}}:password@127.0.0.1:15432/{{cookiecutter.repo_name}} + SECRET_KEY: cj5^uos4tfCdfghjkf5hq$9$(@-79^e9&x$3vyf#igvsfm4d=+ + CELERY_BROKER_URL: redis://localhost:16379 +window_size: + width: 450 + height: 450 +python_versions: + - 2.7.10 +environment: + - approved_platforms: + - linux + - darwin + - freeports: + - 1025 + - 8000 + - 15432 + - 16379 + - brew: + - libtool + - automake + - node + - debs: + - python-setuptools + - python3-dev + - python-virtualenv + - python-pip + - firefox + - automake + - libtool + - libreadline6 + - libreadline6-dev + - libreadline-dev + - libsqlite3-dev + - libpq-dev + - libxml2 + - libxml2-dev + - libssl-dev + - libbz2-dev + - wget + - curl + - llvm + - graphviz-dev + - libtiff4-dev + - libjpeg8-dev + - libfreetype6-dev + - liblcms1-dev + - libwebp-dev + - zlib1g-dev + - gettext + - python-dev + - build-essential diff --git a/{{cookiecutter.repo_name}}/tests/stub.test b/{{cookiecutter.repo_name}}/tests/stub.test new file mode 100644 index 00000000..88b4a1d0 --- /dev/null +++ b/{{cookiecutter.repo_name}}/tests/stub.test @@ -0,0 +1,10 @@ +{% raw %}{% extends "base.yml" %} +{% block test %} +- engine: engine.py:ExecutionEngine + name: Stub {{ python_version }} + preconditions: + python_version: "{{ python_version }}" + scenario: + - Load website + - Pause +{% endblock %}{% endraw %}