├── .github └── workflows │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── example ├── __init__.py ├── elasticsearch.py ├── settings.py └── urls.py ├── manage.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── scripts └── wait-for-it.sh └── tests ├── __init__.py ├── conftest.py └── test_elasticsearch.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Workflow 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: docker-compose build 15 | - run: docker-compose run web ./scripts/wait-for-it.sh elasticsearch_test:9200 -t 60 16 | - run: docker-compose run web poetry run pytest tests 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /app && mkdir /home/app 6 | WORKDIR /app 7 | ENV HOME /home/app 8 | 9 | RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python 10 | ENV PATH="${HOME}/.poetry/bin:${PATH}" 11 | COPY pyproject.toml /app/ 12 | COPY poetry.lock /app/ 13 | RUN poetry install 14 | 15 | COPY . /app 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yanglin Zhao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Pytest Elasticsearch Example 2 | 3 | ## Intro 4 | 5 | When writing tests that interface with databases or distributed caches, I've 6 | found that mocking the database/caches out tends to produce tests that are 7 | brittle and offer very little value, because these mock-based tests usually fail 8 | to capture the subtle nuances in behavior of the system under tests. 9 | 10 | I've found that there's no substitute for testing against the real thing. While 11 | Django offers a good story for 12 | [testing against databases](https://docs.djangoproject.com/en/3.0/topics/testing/overview/#the-test-database), 13 | I couldn't find any good examples of similar strategies for Elasticsearch. 14 | 15 | So here is an example repo that demonstrates how to properly test Python code 16 | that interface with Elasticsearch. We use Django and Pytest specifically in this 17 | instance, but the general idea should readily extend to other languages and 18 | frameworks. 19 | 20 | Read more about the details 21 | [here](https://yanglinzhao.com/posts/test-elasticsearch-in-django). 22 | 23 | ## Running Locally 24 | 25 | If you'd like to run the repo locally, 26 | [`docker-compose`](https://docs.docker.com/compose/install/) is a requirement. 27 | First build the docker image by running the following command: 28 | 29 | ```sh 30 | docker-compose build 31 | ``` 32 | 33 | Next, run the example test inside a docker container: 34 | 35 | ```sh 36 | docker-compose run web poetry run pytest tests 37 | ``` 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | elasticsearch: 5 | image: docker.elastic.co/elasticsearch/elasticsearch:7.8.0 6 | environment: 7 | - cluster.name=docker-cluster 8 | - discovery.type=single-node 9 | - bootstrap.memory_lock=true 10 | - xpack.security.enabled=false 11 | - ES_JAVA_OPTS=-Xms256m -Xmx256m 12 | ulimits: 13 | memlock: 14 | soft: -1 15 | hard: -1 16 | elasticsearch_test: 17 | image: docker.elastic.co/elasticsearch/elasticsearch:7.8.0 18 | environment: 19 | - cluster.name=docker-testing-cluster 20 | - discovery.type=single-node 21 | - bootstrap.memory_lock=true 22 | - xpack.security.enabled=false 23 | - ES_JAVA_OPTS=-Xms128m -Xmx128m 24 | ulimits: 25 | memlock: 26 | soft: -1 27 | hard: -1 28 | web: 29 | build: . 30 | command: poetry run python manage.py runserver 0.0.0.0:8000 31 | volumes: 32 | - .:/app/ 33 | ports: 34 | - "8000:8000" 35 | depends_on: 36 | - elasticsearch 37 | - elasticsearch_test 38 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanglinz/django-pytest-elasticsearch-example/dc94e85aa5193285906468259ceecd6c4b088d76/example/__init__.py -------------------------------------------------------------------------------- /example/elasticsearch.py: -------------------------------------------------------------------------------- 1 | from elasticsearch import Elasticsearch 2 | 3 | from django.conf import settings 4 | 5 | 6 | MOVIE_MAPPING = { 7 | "properties": { 8 | "slug": {"type": "keyword"}, 9 | "title": {"type": "text"}, 10 | "description": {"type": "text"}, 11 | } 12 | } 13 | 14 | SHOW_MAPPING = { 15 | "properties": { 16 | "slug": {"type": "keyword"}, 17 | "title": {"type": "text"}, 18 | "description": {"type": "text"}, 19 | } 20 | } 21 | 22 | 23 | def search_media(query): 24 | """Helper method to get movies and shows based on a search query 25 | """ 26 | client = Elasticsearch(settings.ELASTICSEARCH_HOST) 27 | body = { 28 | "query": {"multi_match": {"query": query, "fields": ["title", "description"]}} 29 | } 30 | response = client.search(index=["movie", "show"], body=body) 31 | return [h["_source"] for h in response["hits"]["hits"]] 32 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Django basics 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | SECRET_KEY = "insecure" 8 | 9 | DEBUG = True 10 | 11 | ALLOWED_HOSTS = [] 12 | 13 | INSTALLED_APPS = [ 14 | "django.contrib.admin", 15 | "django.contrib.auth", 16 | "django.contrib.contenttypes", 17 | "django.contrib.sessions", 18 | "django.contrib.messages", 19 | ] 20 | 21 | MIDDLEWARE = [ 22 | "django.middleware.security.SecurityMiddleware", 23 | "django.contrib.sessions.middleware.SessionMiddleware", 24 | "django.middleware.common.CommonMiddleware", 25 | "django.middleware.csrf.CsrfViewMiddleware", 26 | "django.contrib.auth.middleware.AuthenticationMiddleware", 27 | "django.contrib.messages.middleware.MessageMiddleware", 28 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 29 | ] 30 | 31 | ROOT_URLCONF = "example.urls" 32 | 33 | TEMPLATES = [ 34 | { 35 | "BACKEND": "django.template.backends.django.DjangoTemplates", 36 | "DIRS": [], 37 | "APP_DIRS": True, 38 | "OPTIONS": { 39 | "context_processors": [ 40 | "django.template.context_processors.debug", 41 | "django.template.context_processors.request", 42 | "django.contrib.auth.context_processors.auth", 43 | "django.contrib.messages.context_processors.messages", 44 | ], 45 | }, 46 | }, 47 | ] 48 | 49 | # Database 50 | 51 | DATABASES = { 52 | "default": { 53 | "ENGINE": "django.db.backends.sqlite3", 54 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 55 | } 56 | } 57 | 58 | # Elasticsearch 59 | 60 | ELASTICSEARCH_HOST = os.environ.get("ELASTICSEARCH_HOST", "http://elasticsearch:9200") 61 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.4" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "ASGI specs, helper code, and adapters" 12 | name = "asgiref" 13 | optional = false 14 | python-versions = ">=3.5" 15 | version = "3.2.10" 16 | 17 | [package.extras] 18 | tests = ["pytest", "pytest-asyncio"] 19 | 20 | [[package]] 21 | category = "dev" 22 | description = "Atomic file writes." 23 | marker = "sys_platform == \"win32\"" 24 | name = "atomicwrites" 25 | optional = false 26 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 27 | version = "1.4.0" 28 | 29 | [[package]] 30 | category = "dev" 31 | description = "Classes Without Boilerplate" 32 | name = "attrs" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 35 | version = "19.3.0" 36 | 37 | [package.extras] 38 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 39 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 40 | docs = ["sphinx", "zope.interface"] 41 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 42 | 43 | [[package]] 44 | category = "dev" 45 | description = "The uncompromising code formatter." 46 | name = "black" 47 | optional = false 48 | python-versions = ">=3.6" 49 | version = "19.10b0" 50 | 51 | [package.dependencies] 52 | appdirs = "*" 53 | attrs = ">=18.1.0" 54 | click = ">=6.5" 55 | pathspec = ">=0.6,<1" 56 | regex = "*" 57 | toml = ">=0.9.4" 58 | typed-ast = ">=1.4.0" 59 | 60 | [package.extras] 61 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 62 | 63 | [[package]] 64 | category = "main" 65 | description = "Python package for providing Mozilla's CA Bundle." 66 | name = "certifi" 67 | optional = false 68 | python-versions = "*" 69 | version = "2020.6.20" 70 | 71 | [[package]] 72 | category = "dev" 73 | description = "Composable command line interface toolkit" 74 | name = "click" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 77 | version = "7.1.2" 78 | 79 | [[package]] 80 | category = "dev" 81 | description = "Cross-platform colored terminal text." 82 | marker = "sys_platform == \"win32\"" 83 | name = "colorama" 84 | optional = false 85 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 86 | version = "0.4.3" 87 | 88 | [[package]] 89 | category = "main" 90 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 91 | name = "django" 92 | optional = false 93 | python-versions = ">=3.6" 94 | version = "3.0.8" 95 | 96 | [package.dependencies] 97 | asgiref = ">=3.2,<4.0" 98 | pytz = "*" 99 | sqlparse = ">=0.2.2" 100 | 101 | [package.extras] 102 | argon2 = ["argon2-cffi (>=16.1.0)"] 103 | bcrypt = ["bcrypt"] 104 | 105 | [[package]] 106 | category = "main" 107 | description = "Python client for Elasticsearch" 108 | name = "elasticsearch" 109 | optional = false 110 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 111 | version = "7.8.0" 112 | 113 | [package.dependencies] 114 | certifi = "*" 115 | urllib3 = ">=1.21.1" 116 | 117 | [package.extras] 118 | async = ["aiohttp (>=3,<4)", "yarl"] 119 | develop = ["requests (>=2.0.0,<3.0.0)", "coverage", "mock", "pyyaml", "pytest", "pytest-cov", "sphinx (<1.7)", "sphinx-rtd-theme", "black", "jinja2"] 120 | docs = ["sphinx (<1.7)", "sphinx-rtd-theme"] 121 | requests = ["requests (>=2.4.0,<3.0.0)"] 122 | 123 | [[package]] 124 | category = "dev" 125 | description = "Read metadata from Python packages" 126 | marker = "python_version < \"3.8\"" 127 | name = "importlib-metadata" 128 | optional = false 129 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 130 | version = "1.7.0" 131 | 132 | [package.dependencies] 133 | zipp = ">=0.5" 134 | 135 | [package.extras] 136 | docs = ["sphinx", "rst.linker"] 137 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 138 | 139 | [[package]] 140 | category = "dev" 141 | description = "More routines for operating on iterables, beyond itertools" 142 | name = "more-itertools" 143 | optional = false 144 | python-versions = ">=3.5" 145 | version = "8.4.0" 146 | 147 | [[package]] 148 | category = "dev" 149 | description = "Core utilities for Python packages" 150 | name = "packaging" 151 | optional = false 152 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 153 | version = "20.4" 154 | 155 | [package.dependencies] 156 | pyparsing = ">=2.0.2" 157 | six = "*" 158 | 159 | [[package]] 160 | category = "dev" 161 | description = "Utility library for gitignore style pattern matching of file paths." 162 | name = "pathspec" 163 | optional = false 164 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 165 | version = "0.8.0" 166 | 167 | [[package]] 168 | category = "dev" 169 | description = "plugin and hook calling mechanisms for python" 170 | name = "pluggy" 171 | optional = false 172 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 173 | version = "0.13.1" 174 | 175 | [package.dependencies] 176 | [package.dependencies.importlib-metadata] 177 | python = "<3.8" 178 | version = ">=0.12" 179 | 180 | [package.extras] 181 | dev = ["pre-commit", "tox"] 182 | 183 | [[package]] 184 | category = "dev" 185 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 186 | name = "py" 187 | optional = false 188 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 189 | version = "1.9.0" 190 | 191 | [[package]] 192 | category = "dev" 193 | description = "Python parsing module" 194 | name = "pyparsing" 195 | optional = false 196 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 197 | version = "2.4.7" 198 | 199 | [[package]] 200 | category = "dev" 201 | description = "pytest: simple powerful testing with Python" 202 | name = "pytest" 203 | optional = false 204 | python-versions = ">=3.5" 205 | version = "5.4.3" 206 | 207 | [package.dependencies] 208 | atomicwrites = ">=1.0" 209 | attrs = ">=17.4.0" 210 | colorama = "*" 211 | more-itertools = ">=4.0.0" 212 | packaging = "*" 213 | pluggy = ">=0.12,<1.0" 214 | py = ">=1.5.0" 215 | wcwidth = "*" 216 | 217 | [package.dependencies.importlib-metadata] 218 | python = "<3.8" 219 | version = ">=0.12" 220 | 221 | [package.extras] 222 | checkqa-mypy = ["mypy (v0.761)"] 223 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 224 | 225 | [[package]] 226 | category = "dev" 227 | description = "A Django plugin for pytest." 228 | name = "pytest-django" 229 | optional = false 230 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 231 | version = "3.9.0" 232 | 233 | [package.dependencies] 234 | pytest = ">=3.6" 235 | 236 | [package.extras] 237 | docs = ["sphinx", "sphinx-rtd-theme"] 238 | testing = ["django", "django-configurations (>=2.0)", "six"] 239 | 240 | [[package]] 241 | category = "main" 242 | description = "World timezone definitions, modern and historical" 243 | name = "pytz" 244 | optional = false 245 | python-versions = "*" 246 | version = "2020.1" 247 | 248 | [[package]] 249 | category = "dev" 250 | description = "Alternative regular expression module, to replace re." 251 | name = "regex" 252 | optional = false 253 | python-versions = "*" 254 | version = "2020.7.14" 255 | 256 | [[package]] 257 | category = "dev" 258 | description = "Python 2 and 3 compatibility utilities" 259 | name = "six" 260 | optional = false 261 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 262 | version = "1.15.0" 263 | 264 | [[package]] 265 | category = "main" 266 | description = "Non-validating SQL parser" 267 | name = "sqlparse" 268 | optional = false 269 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 270 | version = "0.3.1" 271 | 272 | [[package]] 273 | category = "dev" 274 | description = "Python Library for Tom's Obvious, Minimal Language" 275 | name = "toml" 276 | optional = false 277 | python-versions = "*" 278 | version = "0.10.1" 279 | 280 | [[package]] 281 | category = "dev" 282 | description = "a fork of Python 2 and 3 ast modules with type comment support" 283 | name = "typed-ast" 284 | optional = false 285 | python-versions = "*" 286 | version = "1.4.1" 287 | 288 | [[package]] 289 | category = "main" 290 | description = "HTTP library with thread-safe connection pooling, file post, and more." 291 | name = "urllib3" 292 | optional = false 293 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 294 | version = "1.25.10" 295 | 296 | [package.extras] 297 | brotli = ["brotlipy (>=0.6.0)"] 298 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] 299 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 300 | 301 | [[package]] 302 | category = "dev" 303 | description = "Measures the displayed width of unicode strings in a terminal" 304 | name = "wcwidth" 305 | optional = false 306 | python-versions = "*" 307 | version = "0.2.5" 308 | 309 | [[package]] 310 | category = "dev" 311 | description = "Backport of pathlib-compatible object wrapper for zip files" 312 | marker = "python_version < \"3.8\"" 313 | name = "zipp" 314 | optional = false 315 | python-versions = ">=3.6" 316 | version = "3.1.0" 317 | 318 | [package.extras] 319 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 320 | testing = ["jaraco.itertools", "func-timeout"] 321 | 322 | [metadata] 323 | content-hash = "c8f25e8835dc4b6c6d6fcb500686dae8335e04ab7acff63910b8cef7fa84eaa5" 324 | lock-version = "1.0" 325 | python-versions = "^3.7" 326 | 327 | [metadata.files] 328 | appdirs = [ 329 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 330 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 331 | ] 332 | asgiref = [ 333 | {file = "asgiref-3.2.10-py3-none-any.whl", hash = "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"}, 334 | {file = "asgiref-3.2.10.tar.gz", hash = "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a"}, 335 | ] 336 | atomicwrites = [ 337 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 338 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 339 | ] 340 | attrs = [ 341 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 342 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 343 | ] 344 | black = [ 345 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 346 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 347 | ] 348 | certifi = [ 349 | {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, 350 | {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, 351 | ] 352 | click = [ 353 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 354 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 355 | ] 356 | colorama = [ 357 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 358 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 359 | ] 360 | django = [ 361 | {file = "Django-3.0.8-py3-none-any.whl", hash = "sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"}, 362 | {file = "Django-3.0.8.tar.gz", hash = "sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582"}, 363 | ] 364 | elasticsearch = [ 365 | {file = "elasticsearch-7.8.0-py2.py3-none-any.whl", hash = "sha256:6fb566dd23b91b5871ce12212888674b4cf33374e92b71b1080916c931e44dcb"}, 366 | {file = "elasticsearch-7.8.0.tar.gz", hash = "sha256:e637d8cf4e27e279b5ff8ca8edc0c086f4b5df4bf2b48e2f950b7833aca3a792"}, 367 | ] 368 | importlib-metadata = [ 369 | {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, 370 | {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, 371 | ] 372 | more-itertools = [ 373 | {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, 374 | {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, 375 | ] 376 | packaging = [ 377 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 378 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 379 | ] 380 | pathspec = [ 381 | {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, 382 | {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, 383 | ] 384 | pluggy = [ 385 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 386 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 387 | ] 388 | py = [ 389 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 390 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 391 | ] 392 | pyparsing = [ 393 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 394 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 395 | ] 396 | pytest = [ 397 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 398 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 399 | ] 400 | pytest-django = [ 401 | {file = "pytest-django-3.9.0.tar.gz", hash = "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"}, 402 | {file = "pytest_django-3.9.0-py2.py3-none-any.whl", hash = "sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5"}, 403 | ] 404 | pytz = [ 405 | {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, 406 | {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, 407 | ] 408 | regex = [ 409 | {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, 410 | {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, 411 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, 412 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, 413 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, 414 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, 415 | {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, 416 | {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, 417 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, 418 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, 419 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, 420 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, 421 | {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, 422 | {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, 423 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, 424 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, 425 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, 426 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, 427 | {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, 428 | {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, 429 | {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, 430 | ] 431 | six = [ 432 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 433 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 434 | ] 435 | sqlparse = [ 436 | {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, 437 | {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, 438 | ] 439 | toml = [ 440 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 441 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 442 | ] 443 | typed-ast = [ 444 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 445 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 446 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 447 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 448 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 449 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 450 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 451 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 452 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 453 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 454 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 455 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 456 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 457 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 458 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 459 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 460 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 461 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 462 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 463 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 464 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 465 | ] 466 | urllib3 = [ 467 | {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, 468 | {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, 469 | ] 470 | wcwidth = [ 471 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 472 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 473 | ] 474 | zipp = [ 475 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 476 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 477 | ] 478 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-pytest-elasticsearch-example" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Yanglin Zhao "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | django = "^3.0.8" 11 | elasticsearch = "^7.8.0" 12 | 13 | [tool.poetry.dev-dependencies] 14 | black = "^19.10b0" 15 | pytest = "^5.4.3" 16 | pytest-django = "^3.9.0" 17 | 18 | [build-system] 19 | requires = ["poetry>=0.12"] 20 | build-backend = "poetry.masonry.api" 21 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = example.settings 3 | -------------------------------------------------------------------------------- /scripts/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # Check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | 148 | WAITFORIT_BUSYTIMEFLAG="" 149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 150 | WAITFORIT_ISBUSY=1 151 | # Check if busybox timeout uses -t flag 152 | # (recent Alpine versions don't support -t anymore) 153 | if timeout &>/dev/stdout | grep -q -e '-t '; then 154 | WAITFORIT_BUSYTIMEFLAG="-t" 155 | fi 156 | else 157 | WAITFORIT_ISBUSY=0 158 | fi 159 | 160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 161 | wait_for 162 | WAITFORIT_RESULT=$? 163 | exit $WAITFORIT_RESULT 164 | else 165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | WAITFORIT_RESULT=$? 168 | else 169 | wait_for 170 | WAITFORIT_RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $WAITFORIT_CLI != "" ]]; then 175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 177 | exit $WAITFORIT_RESULT 178 | fi 179 | exec "${WAITFORIT_CLI[@]}" 180 | else 181 | exit $WAITFORIT_RESULT 182 | fi 183 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanglinz/django-pytest-elasticsearch-example/dc94e85aa5193285906468259ceecd6c4b088d76/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from elasticsearch import Elasticsearch 4 | 5 | from example.elasticsearch import MOVIE_MAPPING, SHOW_MAPPING 6 | 7 | schemas = { 8 | "movie": MOVIE_MAPPING, 9 | "show": SHOW_MAPPING, 10 | } 11 | 12 | ELASTICSEARCH_TEST_HOST = "http://elasticsearch_test:9200" 13 | 14 | 15 | def setup_elasticsearch(): 16 | es = Elasticsearch(ELASTICSEARCH_TEST_HOST) 17 | for index_name, schema in schemas.items(): 18 | body = { 19 | "settings": { 20 | "number_of_shards": 1, 21 | "number_of_replicas": 1, 22 | "index.store.type": "mmapfs", 23 | }, 24 | "mappings": schema, 25 | } 26 | es.indices.create(index=index_name, body=body) 27 | 28 | 29 | def teardown_elasticsearch(): 30 | es = Elasticsearch(ELASTICSEARCH_TEST_HOST) 31 | for index_name in schemas.keys(): 32 | es.indices.delete(index=index_name) 33 | 34 | 35 | @pytest.fixture 36 | def elasticsearch(settings): 37 | settings.ELASTICSEARCH_HOST = ELASTICSEARCH_TEST_HOST 38 | 39 | setup_elasticsearch() 40 | yield Elasticsearch(ELASTICSEARCH_TEST_HOST) 41 | teardown_elasticsearch() 42 | -------------------------------------------------------------------------------- /tests/test_elasticsearch.py: -------------------------------------------------------------------------------- 1 | from example.elasticsearch import search_media 2 | 3 | 4 | def index_test_fixtures(es, index_name, data): 5 | created = es.index(index=index_name, body=data) 6 | assert created["result"] == "created" 7 | es.indices.refresh(index_name) 8 | 9 | 10 | class TestElasticsearch: 11 | def test_elasticsearch(self, elasticsearch): 12 | # Setup test fixtures 13 | index_test_fixtures( 14 | elasticsearch, 15 | "movie", 16 | { 17 | "slug": "episode-5", 18 | "title": "Star Wars: Episode V - The Empire Strikes Back", 19 | "description": "After the Rebels are brutally overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jedi training with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", 20 | }, 21 | ) 22 | index_test_fixtures( 23 | elasticsearch, 24 | "movie", 25 | { 26 | "slug": "episode-3", 27 | "title": "Star Wars: Episode III - Revenge of the Sith", 28 | "description": "Three years into the Clone Wars, the Jedi rescue Palpatine from Count Dooku. As Obi-Wan pursues a new threat, Anakin acts as a double agent between the Jedi Council and Palpatine and is lured into a sinister plan to rule the galaxy.", 29 | }, 30 | ) 31 | index_test_fixtures( 32 | elasticsearch, 33 | "movie", 34 | { 35 | "slug": "rouge-one", 36 | "title": "Rogue One: A Star Wars Story", 37 | "description": "The daughter of an Imperial scientist joins the Rebel Alliance in a risky move to steal the Death Star plans.", 38 | }, 39 | ) 40 | 41 | index_test_fixtures( 42 | elasticsearch, 43 | "show", 44 | { 45 | "slug": "clone-wars", 46 | "title": "Star Wars: The Clone Wars", 47 | "description": "Jedi Knights lead the Grand Army of the Republic against the droid army of the Separatists.", 48 | }, 49 | ) 50 | index_test_fixtures( 51 | elasticsearch, 52 | "show", 53 | { 54 | "slug": "the-mandalorian", 55 | "title": "The Mandalorian", 56 | "description": "The travels of a lone bounty hunter in the outer reaches of the galaxy, far from the authority of the New Republic.", 57 | }, 58 | ) 59 | 60 | # Test search helper 61 | results = search_media("Star Wars") 62 | results = {r["slug"] for r in results} 63 | assert results == {"episode-5", "episode-3", "rouge-one", "clone-wars"} 64 | 65 | results = search_media("Jedi") 66 | results = {r["slug"] for r in results} 67 | assert results == {"episode-5", "episode-3", "clone-wars"} 68 | 69 | results = search_media("Galaxy") 70 | results = {r["slug"] for r in results} 71 | assert results == {"episode-5", "episode-3", "the-mandalorian"} 72 | --------------------------------------------------------------------------------