From 1450a7de57909251af2940b0e8181e43b39f1b07 Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 14:34:42 +0400 Subject: [PATCH 01/11] add simple asserts and main test case --- .github/workflows/run-tests.yml | 2 - CHECKLIST.md | 14 +- Makefile | 9 +- {testing => demo}/__init__.py | 0 demo/admin.py | 3 + demo/apps.py | 6 + .../migrations}/__init__.py | 0 demo/models.py | 3 + demo/templates/demo/index.html | 1 + demo/tests_django.py | 28 ++++ demo/tests_dry.py | 106 +++++++++++++++ demo/urls.py | 11 ++ demo/views.py | 11 ++ dev-requirements.txt | 2 - dry_tests/__init__.py | 2 + dry_tests/admin.py | 3 + dry_tests/apps.py | 6 + dry_tests/migrations/__init__.py | 0 dry_tests/models.py | 20 +++ dry_tests/testcases.py | 46 +++++-- dry_tests/tests.py | 3 + manage.py | 22 +++ settings/__init__.py | 0 settings/asgi.py | 16 +++ settings/settings.py | 125 ++++++++++++++++++ settings/urls.py | 10 ++ settings/wsgi.py | 16 +++ testing/test_dry_tests/test_testcases.py | 12 -- 28 files changed, 443 insertions(+), 34 deletions(-) rename {testing => demo}/__init__.py (100%) create mode 100644 demo/admin.py create mode 100644 demo/apps.py rename {testing/test_dry_tests => demo/migrations}/__init__.py (100%) create mode 100644 demo/models.py create mode 100644 demo/templates/demo/index.html create mode 100644 demo/tests_django.py create mode 100644 demo/tests_dry.py create mode 100644 demo/urls.py create mode 100644 demo/views.py create mode 100644 dry_tests/admin.py create mode 100644 dry_tests/apps.py create mode 100644 dry_tests/migrations/__init__.py create mode 100644 dry_tests/models.py create mode 100644 dry_tests/tests.py create mode 100755 manage.py create mode 100644 settings/__init__.py create mode 100644 settings/asgi.py create mode 100644 settings/settings.py create mode 100644 settings/urls.py create mode 100644 settings/wsgi.py delete mode 100644 testing/test_dry_tests/test_testcases.py diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1c9bbc6..d26d65d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -21,8 +21,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest - pip install pytest-cov if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with pytest run: | diff --git a/CHECKLIST.md b/CHECKLIST.md index f72360c..5b3c471 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -29,12 +29,12 @@ - [X] [Security Policy](https://github.com/quillcraftsman/open-source-checklist#security-policy) [CI and CD](https://github.com/quillcraftsman/open-source-checklist#ci-and-cd) -- [ ] Tests -- [ ] Test Coverage -- [ ] Test Coverage 100% -- [ ] Linters -- [ ] Build -- [ ] Deploy To PyPi +- [x] Tests +- [x] Test Coverage +- [x] Test Coverage 100% +- [x] Linters +- [x] Build +- [x] Deploy To PyPi Others -- [ ] Makefile? \ No newline at end of file +- [x] Makefile \ No newline at end of file diff --git a/Makefile b/Makefile index 731f44e..256727e 100755 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ test: - pytest + python manage.py test + +server: + python manage.py runserver coverage: - pytest -s --cov --cov-report html --cov-fail-under 100 + coverage run --source='.' manage.py test + coverage report --omit=settings/asgi.py,settings/wsgi.py,manage.py,setup.py --fail-under=100 + coverage html --omit=settings/asgi.py,settings/wsgi.py,manage.py,setup.py yamllint: yamllint -d relaxed . diff --git a/testing/__init__.py b/demo/__init__.py similarity index 100% rename from testing/__init__.py rename to demo/__init__.py diff --git a/demo/admin.py b/demo/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/demo/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/demo/apps.py b/demo/apps.py new file mode 100644 index 0000000..d69e874 --- /dev/null +++ b/demo/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DemoConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'demo' diff --git a/testing/test_dry_tests/__init__.py b/demo/migrations/__init__.py similarity index 100% rename from testing/test_dry_tests/__init__.py rename to demo/migrations/__init__.py diff --git a/demo/models.py b/demo/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/demo/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/demo/templates/demo/index.html b/demo/templates/demo/index.html new file mode 100644 index 0000000..25c25d5 --- /dev/null +++ b/demo/templates/demo/index.html @@ -0,0 +1 @@ +{{title}} \ No newline at end of file diff --git a/demo/tests_django.py b/demo/tests_django.py new file mode 100644 index 0000000..8638748 --- /dev/null +++ b/demo/tests_django.py @@ -0,0 +1,28 @@ +""" +Tests with django TestCase +""" +from django.test import TestCase + + +class ViewTestCase(TestCase): + + def setUp(self): + self.url = '/' + + def test_status_code_get(self): + self.assertEqual(self.client.get(self.url).status_code, 200) + + def test_status_code_post(self): + self.assertEqual(self.client.post(self.url).status_code, 302) + + def test_redirect_url(self): + self.assertRedirects(self.client.post(self.url), '/') + + def test_value_in_context(self): + self.assertIn('title', self.client.get(self.url).context) + + def test_context_value(self): + self.assertEqual('Title', self.client.get(self.url).context['title']) + + def test_value_in_content(self): + self.assertContains(self.client.get(self.url), 'Title', 1) diff --git a/demo/tests_dry.py b/demo/tests_dry.py new file mode 100644 index 0000000..514e4c3 --- /dev/null +++ b/demo/tests_dry.py @@ -0,0 +1,106 @@ +""" +Tests with Django Dry Tests TestCase +""" +from dry_tests import Request, Response, TestCase, POST + + +class ViewTestCase(TestCase): + + def test_main(self): + data = [ + # RedirectUrl + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302, redirect_url='/'), + 'should_fail': False, + 'assert': self.assertRedirectUrl, + }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302, redirect_url='/fail_redirect/'), + 'should_fail': True, + 'assert': self.assertRedirectUrl, + }, + # Post StatusCode + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302), + 'should_fail': False, + 'assert': self.assertStatusCode, + }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=404), + 'should_fail': True, + 'assert': self.assertStatusCode, + }, + # Get StatusCode + { + 'request' : Request(url='/'), + 'response' : Response(status_code=200), + 'should_fail': False, + 'assert': self.assertStatusCode, + }, + { + 'request': Request(url='/'), + 'response': Response(status_code=404), + 'should_fail': True, + 'assert': self.assertStatusCode, + }, + # Value in Context + { + 'request': Request(url='/'), + 'response': Response(in_context='title'), + 'should_fail': False, + 'assert': self.assertValueInContext, + }, + { + 'request': Request(url='/'), + 'response': Response(in_context='not_in_context_key'), + 'should_fail': True, + 'assert': self.assertValueInContext, + }, + # Context Value + { + 'request': Request(url='/'), + 'response': Response(context_values={'title': 'Title'}), + 'should_fail': False, + 'assert': self.assertContextValues, + }, + { + 'request': Request(url='/'), + 'response': Response(context_values={'title': 'Error value'}), + 'should_fail': True, + 'assert': self.assertContextValues, + }, + { + 'request': Request(url='/'), + 'response': Response(context_values={'error key': 'Title'}), + 'should_fail': True, + 'assert': self.assertContextValues, + }, + # Content Value + { + 'request': Request(url='/'), + 'response': Response(content_value='Title'), + 'should_fail': False, + 'assert': self.assertContentValue, + }, + { + 'request': Request(url='/'), + 'response': Response(content_value='Error value'), + 'should_fail': True, + 'assert': self.assertContentValue, + }, + ] + + for item in data: + request = item['request'] + response = item['response'] + assert_function = item['assert'] + with self.subTest(msg=str(item)): + if item['should_fail']: + with self.assertRaises(AssertionError): + assert_function(item['request'], item['response']) + else: + assert_function(request, response) \ No newline at end of file diff --git a/demo/urls.py b/demo/urls.py new file mode 100644 index 0000000..91183bd --- /dev/null +++ b/demo/urls.py @@ -0,0 +1,11 @@ +""" +URL configuration for settings project. +""" +from django.urls import path +from . import views + +app_name = 'demo' + +urlpatterns = [ + path('', views.index_view), +] \ No newline at end of file diff --git a/demo/views.py b/demo/views.py new file mode 100644 index 0000000..309aa3c --- /dev/null +++ b/demo/views.py @@ -0,0 +1,11 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import render + + +def index_view(request): + if request.method == 'GET': + context = { + 'title': 'Title' + } + return render(request, 'demo/index.html', context) + return HttpResponseRedirect('/') diff --git a/dev-requirements.txt b/dev-requirements.txt index a420bbd..a871eaa 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,2 @@ -pytest==7.4.2 -pytest-cov==4.1.0 yamllint==1.32.0 pylint==3.0.1 \ No newline at end of file diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index e69de29..f130fa4 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -0,0 +1,2 @@ +from .testcases import TestCase +from .models import Request, Response, GET, POST diff --git a/dry_tests/admin.py b/dry_tests/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dry_tests/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dry_tests/apps.py b/dry_tests/apps.py new file mode 100644 index 0000000..a23ddb7 --- /dev/null +++ b/dry_tests/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DryTestsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dry_tests' diff --git a/dry_tests/migrations/__init__.py b/dry_tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dry_tests/models.py b/dry_tests/models.py new file mode 100644 index 0000000..0d76da3 --- /dev/null +++ b/dry_tests/models.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Literal + +GET = 'get' +POST = 'post' + + +@dataclass(frozen=True) +class Request: + url: str + method: Literal[GET, POST] = GET + + +@dataclass(frozen=True) +class Response: + status_code: int = 200 + redirect_url: str = None + in_context: str = None + context_values: dict = None + content_value: str = None diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index 0d8a8c7..69fa14a 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -1,11 +1,39 @@ -""" -Test Cases -""" +from django.test import TestCase as DjangoTestCase +from .models import GET, POST -def mock(): - """ - Mock method to run ci/cd - :return: Constant - """ - return 'mock' +class TestCase(DjangoTestCase): + + def get_url_response(self, request): + requests = { + GET: self.client.get, + POST: self.client.post + } + + url_response = requests[request.method](request.url) + return url_response + + def assertStatusCode(self, request, response): + url_response = self.get_url_response(request) + self.assertEqual(url_response.status_code, response.status_code) + + def assertRedirectUrl(self, request, response): + url_response = self.get_url_response(request) + self.assertRedirects(url_response, response.redirect_url) + + def assertValueInContext(self, request, response): + url_response = self.get_url_response(request) + self.assertIn(response.in_context, url_response.context) + + def assertContextValues(self, request, response): + url_response = self.get_url_response(request) + context = url_response.context + context_values = response.context_values + for key, value in context_values.items(): + self.assertIn(key, context) + self.assertEqual(value, context[key]) + + def assertContentValue(self, request, response): + # TODO: Response send every time - it's not good + url_response = self.get_url_response(request) + self.assertContains(url_response, response.content_value) \ No newline at end of file diff --git a/dry_tests/tests.py b/dry_tests/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dry_tests/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..5767e97 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/settings/__init__.py b/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/settings/asgi.py b/settings/asgi.py new file mode 100644 index 0000000..48f9613 --- /dev/null +++ b/settings/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for settings project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + +application = get_asgi_application() diff --git a/settings/settings.py b/settings/settings.py new file mode 100644 index 0000000..4e5ed01 --- /dev/null +++ b/settings/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for settings project. + +Generated by 'django-admin startproject' using Django 4.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-ax)@6&7595p!d1#h%p(8p+z$e(h6d@0&@u=ev1+drzc6z9_i62' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'dry_tests', + 'demo', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'settings.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'settings.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/settings/urls.py b/settings/urls.py new file mode 100644 index 0000000..ed141bc --- /dev/null +++ b/settings/urls.py @@ -0,0 +1,10 @@ +""" +URL configuration for settings project. +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('demo.urls')) +] diff --git a/settings/wsgi.py b/settings/wsgi.py new file mode 100644 index 0000000..9c9a1c8 --- /dev/null +++ b/settings/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for settings project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + +application = get_wsgi_application() diff --git a/testing/test_dry_tests/test_testcases.py b/testing/test_dry_tests/test_testcases.py deleted file mode 100644 index bccdf95..0000000 --- a/testing/test_dry_tests/test_testcases.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Test for testcases -""" -from dry_tests.testcases import mock - - -def test_mock(): - """ - Test mock - :return: - """ - assert mock() == 'mock' From 5c8adbe6c3415036260447da80e83b33e2fad4cf Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 15:31:04 +0400 Subject: [PATCH 02/11] fix tests and linters --- .github/workflows/run-tests.yml | 1 + demo/admin.py | 2 +- demo/models.py | 2 +- demo/tests_dry.py | 2 +- demo/urls.py | 2 +- dev-requirements.txt | 3 +- dry_tests/admin.py | 3 - dry_tests/testcases.py | 2 +- dry_tests/tests.py | 3 - manage.py | 2 +- pylintrc | 652 ++++++++++++++++++++++++++++++++ 11 files changed, 661 insertions(+), 13 deletions(-) delete mode 100644 dry_tests/admin.py delete mode 100644 dry_tests/tests.py create mode 100644 pylintrc diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d26d65d..e2e8257 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -21,6 +21,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r dev-requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with pytest run: | diff --git a/demo/admin.py b/demo/admin.py index 8c38f3f..4185d36 100644 --- a/demo/admin.py +++ b/demo/admin.py @@ -1,3 +1,3 @@ -from django.contrib import admin +# from django.contrib import admin # Register your models here. diff --git a/demo/models.py b/demo/models.py index 71a8362..0b4331b 100644 --- a/demo/models.py +++ b/demo/models.py @@ -1,3 +1,3 @@ -from django.db import models +# from django.db import models # Create your models here. diff --git a/demo/tests_dry.py b/demo/tests_dry.py index 514e4c3..95f2a57 100644 --- a/demo/tests_dry.py +++ b/demo/tests_dry.py @@ -103,4 +103,4 @@ def test_main(self): with self.assertRaises(AssertionError): assert_function(item['request'], item['response']) else: - assert_function(request, response) \ No newline at end of file + assert_function(request, response) diff --git a/demo/urls.py b/demo/urls.py index 91183bd..a63f070 100644 --- a/demo/urls.py +++ b/demo/urls.py @@ -8,4 +8,4 @@ urlpatterns = [ path('', views.index_view), -] \ No newline at end of file +] diff --git a/dev-requirements.txt b/dev-requirements.txt index a871eaa..4512edc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ yamllint==1.32.0 -pylint==3.0.1 \ No newline at end of file +pylint==3.0.1 +coverage==7.3.2 \ No newline at end of file diff --git a/dry_tests/admin.py b/dry_tests/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/dry_tests/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index 69fa14a..b0e7ee8 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -36,4 +36,4 @@ def assertContextValues(self, request, response): def assertContentValue(self, request, response): # TODO: Response send every time - it's not good url_response = self.get_url_response(request) - self.assertContains(url_response, response.content_value) \ No newline at end of file + self.assertContains(url_response, response.content_value) diff --git a/dry_tests/tests.py b/dry_tests/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/dry_tests/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/manage.py b/manage.py index 5767e97..7eb6b0d 100755 --- a/manage.py +++ b/manage.py @@ -8,7 +8,7 @@ def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') try: - from django.core.management import execute_from_command_line + from django.core.management import execute_from_command_line # pylint: disable=import-outside-toplevel except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..b050377 --- /dev/null +++ b/pylintrc @@ -0,0 +1,652 @@ +[MAIN] +disable= + E1101, # Dynamic attributes + C0301, # Line too long + E0307, # __str__ does not return str + W0223, # method is not overriden in child class + W0613, # unused argument + C0103, # snake case (unittests) + W0511, # fixme + + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + import-error, + wildcard-import, + import-error, + unused-wildcard-import, + too-few-public-methods, + invalid-name, + unused-variable, + import-outside-toplevel, + duplicate-code + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io From 8edd93326149b7a6de5fb3b8e78e811013fca2f8 Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 18:51:59 +0400 Subject: [PATCH 03/11] start to design database state to the testcase --- Makefile | 3 + demo/apps.py | 6 + demo/migrations/0001_initial.py | 21 ++ demo/models.py | 12 +- demo/tests/__init__.py | 0 demo/tests/db_test_data.py | 10 + demo/tests/tests_django.py | 92 +++++ demo/{ => tests}/tests_dry.py | 73 ++++ demo/tests_django.py | 28 -- demo/urls.py | 1 + demo/views.py | 23 ++ dry_tests/__init__.py | 3 + dry_tests/apps.py | 6 + dry_tests/models.py | 13 + dry_tests/testcases.py | 61 ++- pylintrc | 644 +------------------------------- 16 files changed, 322 insertions(+), 674 deletions(-) create mode 100644 demo/migrations/0001_initial.py create mode 100644 demo/tests/__init__.py create mode 100644 demo/tests/db_test_data.py create mode 100644 demo/tests/tests_django.py rename demo/{ => tests}/tests_dry.py (62%) delete mode 100644 demo/tests_django.py diff --git a/Makefile b/Makefile index 256727e..988e9a0 100755 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ test: server: python manage.py runserver +makemigrations: + python manage.py makemigrations + coverage: coverage run --source='.' manage.py test coverage report --omit=settings/asgi.py,settings/wsgi.py,manage.py,setup.py --fail-under=100 diff --git a/demo/apps.py b/demo/apps.py index d69e874..9711043 100644 --- a/demo/apps.py +++ b/demo/apps.py @@ -1,6 +1,12 @@ +""" +Demo app module +""" from django.apps import AppConfig class DemoConfig(AppConfig): + """ + Demo app config class + """ default_auto_field = 'django.db.models.BigAutoField' name = 'demo' diff --git a/demo/migrations/0001_initial.py b/demo/migrations/0001_initial.py new file mode 100644 index 0000000..e94442d --- /dev/null +++ b/demo/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.6 on 2023-10-11 11:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Simple', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, unique=True)), + ], + ), + ] diff --git a/demo/models.py b/demo/models.py index 0b4331b..75faa67 100644 --- a/demo/models.py +++ b/demo/models.py @@ -1,3 +1,11 @@ -# from django.db import models +""" +Demo models +""" +from django.db import models -# Create your models here. + +class Simple(models.Model): + """ + Simple model with unique name + """ + name = models.CharField(max_length=32, unique=True) diff --git a/demo/tests/__init__.py b/demo/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/tests/db_test_data.py b/demo/tests/db_test_data.py new file mode 100644 index 0000000..19cc235 --- /dev/null +++ b/demo/tests/db_test_data.py @@ -0,0 +1,10 @@ +# from demo.models import Simple +# +# +# def simple_start(): +# Simple.objects.create(name='first') +# +# +# def create_simple(): +# simple = Simple.objects.create(name='new name') +# return simple diff --git a/demo/tests/tests_django.py b/demo/tests/tests_django.py new file mode 100644 index 0000000..a9d744c --- /dev/null +++ b/demo/tests/tests_django.py @@ -0,0 +1,92 @@ +""" +Tests with django TestCase +""" +from django.test import TestCase +from demo.models import Simple + + +class ViewTestCase(TestCase): + """ + Django Test Case to Test views + """ + + def setUp(self): + """ + Set Up test data + :return: + """ + self.url = '/' + + def test_status_code_get(self): + """ + Test get status code + :return: + """ + self.assertEqual(self.client.get(self.url).status_code, 200) + + def test_status_code_post(self): + """ + Test post status code + :return: + """ + self.assertEqual(self.client.post(self.url).status_code, 302) + + def test_redirect_url(self): + """ + Test Success Redirect + :return: + """ + self.assertRedirects(self.client.post(self.url), '/') + + def test_value_in_context(self): + """ + Test Context Contains Value + :return: + """ + self.assertIn('title', self.client.get(self.url).context) + + def test_context_value(self): + """ + Test Context Value + :return: + """ + self.assertEqual('Title', self.client.get(self.url).context['title']) + + def test_value_in_content(self): + """ + Test Value in Content + :return: + """ + self.assertContains(self.client.get(self.url), 'Title', 1) + + def test_new_object_created(self): + """ + Test New Object Was Created + :return: + """ + data = { + 'name': 'new_name' + } + self.assertFalse(Simple.objects.filter(name='new_name').exists()) + self.client.post(self.url, data=data) + self.assertTrue(Simple.objects.filter(name='new_name').exists()) + + # def test_object_updated(self): + # first = Simple.objects.create(name='first') + # url = f'/{first.pk}/' + # data = { + # 'name': 'updated_name' + # } + # self.assertTrue(Simple.objects.filter(name='first').exists()) + # self.client.post(url, data=data) + # self.assertTrue(Simple.objects.filter(name='updated_name').exists()) + + def test_detail_view(self): + """ + Test Detail View + :return: + """ + first = Simple.objects.create(name='first') + url = f'/{first.pk}/' + response = self.client.get(url) + self.assertEqual(response.status_code, 200) diff --git a/demo/tests_dry.py b/demo/tests/tests_dry.py similarity index 62% rename from demo/tests_dry.py rename to demo/tests/tests_dry.py index 95f2a57..f4d055d 100644 --- a/demo/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -2,11 +2,27 @@ Tests with Django Dry Tests TestCase """ from dry_tests import Request, Response, TestCase, POST +# from .db_test_data import create_simple +from demo.models import Simple class ViewTestCase(TestCase): + """ + Concrete TestCase inherited from DRY TestCase + """ + + def clear_db(self): + """ + Clear database method for subtests + :return: + """ + Simple.objects.all().delete() def test_main(self): + """ + All asserts tests in one test function + :return: + """ data = [ # RedirectUrl { @@ -92,6 +108,56 @@ def test_main(self): 'should_fail': True, 'assert': self.assertContentValue, }, + # Create object + { + 'request': Request( + url='/', + method=POST, + data={'name': 'new_name'} + ), + 'response': Response( + created={ + 'model': Simple, + 'fields': { + 'name': 'new_name', + } + }, + ), + 'should_fail': False, + 'assert': self.assertCreated, + }, + { + 'request': Request( + url='/', + method=POST, + data={'name': 'new_name'} + ), + 'response': Response( + created={ + 'model': Simple, + 'fields': { + 'name': 'error_name', + } + }, + ), + 'should_fail': True, + 'assert': self.assertCreated, + }, + # Test Detail View + # { + # 'request': Request( + # url='/', + # create_one={ + # 'model': Simple, + # 'fields': { + # 'name': 'some name', + # } + # } + # ), + # 'response': Response(status_code=200), + # 'should_fail': False, + # 'assert': self.assertStatusCode, + # }, ] for item in data: @@ -99,8 +165,15 @@ def test_main(self): response = item['response'] assert_function = item['assert'] with self.subTest(msg=str(item)): + # self.setUp() + # self.setUpClass() + # self.setUpTestData() + # self.clear_db() if item['should_fail']: with self.assertRaises(AssertionError): assert_function(item['request'], item['response']) else: assert_function(request, response) + self.clear_db() # TODO: someting wrong with supTests + # self.tearDown() + # self.tearDownClass() diff --git a/demo/tests_django.py b/demo/tests_django.py deleted file mode 100644 index 8638748..0000000 --- a/demo/tests_django.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Tests with django TestCase -""" -from django.test import TestCase - - -class ViewTestCase(TestCase): - - def setUp(self): - self.url = '/' - - def test_status_code_get(self): - self.assertEqual(self.client.get(self.url).status_code, 200) - - def test_status_code_post(self): - self.assertEqual(self.client.post(self.url).status_code, 302) - - def test_redirect_url(self): - self.assertRedirects(self.client.post(self.url), '/') - - def test_value_in_context(self): - self.assertIn('title', self.client.get(self.url).context) - - def test_context_value(self): - self.assertEqual('Title', self.client.get(self.url).context['title']) - - def test_value_in_content(self): - self.assertContains(self.client.get(self.url), 'Title', 1) diff --git a/demo/urls.py b/demo/urls.py index a63f070..27e15c8 100644 --- a/demo/urls.py +++ b/demo/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ path('', views.index_view), + path('/', views.one_view), ] diff --git a/demo/views.py b/demo/views.py index 309aa3c..173a30d 100644 --- a/demo/views.py +++ b/demo/views.py @@ -1,11 +1,34 @@ +""" +Views for test main features +""" from django.http import HttpResponseRedirect from django.shortcuts import render +from .models import Simple def index_view(request): + """ + View for list and greate object + :param request: + :return: + """ if request.method == 'GET': context = { 'title': 'Title' } return render(request, 'demo/index.html', context) + + name = request.POST.get('name', None) + if name is not None: + Simple.objects.create(name=name) return HttpResponseRedirect('/') + + +def one_view(request, pk): + """ + One view with pk to detail, update and delete object + :param request: + :param pk: + :return: + """ + return render(request, 'demo/index.html') diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index f130fa4..d8589ae 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -1,2 +1,5 @@ +""" +Init file for dry_test package +""" from .testcases import TestCase from .models import Request, Response, GET, POST diff --git a/dry_tests/apps.py b/dry_tests/apps.py index a23ddb7..ba81dbf 100644 --- a/dry_tests/apps.py +++ b/dry_tests/apps.py @@ -1,6 +1,12 @@ +""" +DRY Tests django app +""" from django.apps import AppConfig class DryTestsConfig(AppConfig): + """ + Dry Tests App Config Class + """ default_auto_field = 'django.db.models.BigAutoField' name = 'dry_tests' diff --git a/dry_tests/models.py b/dry_tests/models.py index 0d76da3..b7299ad 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -1,5 +1,9 @@ +""" +Main models to DRY tests +""" from dataclasses import dataclass from typing import Literal +from django.db.models import Model GET = 'get' POST = 'post' @@ -7,14 +11,23 @@ @dataclass(frozen=True) class Request: + """ + Main Request Model + """ url: str method: Literal[GET, POST] = GET + data: dict = None @dataclass(frozen=True) class Response: + """ + Main Excepted Response Model + """ status_code: int = 200 redirect_url: str = None in_context: str = None context_values: dict = None content_value: str = None + created: Model = None + # db_data: callable = None diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index b0e7ee8..c9042ce 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -1,31 +1,70 @@ +""" +Base testcases +""" from django.test import TestCase as DjangoTestCase from .models import GET, POST class TestCase(DjangoTestCase): + """ + Main TestCase with test database + """ def get_url_response(self, request): + """ + get response with test client + :param request: Request model + :return: client response + """ requests = { GET: self.client.get, POST: self.client.post } - url_response = requests[request.method](request.url) + # if request.data is None: + # url_response = requests[request.method](request.url) + # else: + url_response = requests[request.method](request.url, data=request.data) + return url_response def assertStatusCode(self, request, response): + """ + Check status code + :param request: Request + :param response: Response + :return: None + """ url_response = self.get_url_response(request) self.assertEqual(url_response.status_code, response.status_code) def assertRedirectUrl(self, request, response): + """ + Check Redirect Url + :param request: Request + :param response: Response + :return: None + """ url_response = self.get_url_response(request) self.assertRedirects(url_response, response.redirect_url) def assertValueInContext(self, request, response): + """ + Check Value In Context + :param request: Request + :param response: Response + :return: None + """ url_response = self.get_url_response(request) self.assertIn(response.in_context, url_response.context) def assertContextValues(self, request, response): + """ + Check Context Value + :param request: Request + :param response: Response + :return: None + """ url_response = self.get_url_response(request) context = url_response.context context_values = response.context_values @@ -34,6 +73,26 @@ def assertContextValues(self, request, response): self.assertEqual(value, context[key]) def assertContentValue(self, request, response): + """ + Check Content Value + :param request: Request + :param response: Response + :return: None + """ # TODO: Response send every time - it's not good url_response = self.get_url_response(request) self.assertContains(url_response, response.content_value) + + def assertCreated(self, request, response): + """ + Check object was created + :param request: Request + :param response: Response + :return: None + """ + created = response.created + model = created['model'] + fields = created['fields'] + self.assertFalse(model.objects.filter(**fields).exists()) + self.get_url_response(request) + self.assertTrue(model.objects.filter(**fields).exists()) diff --git a/pylintrc b/pylintrc index b050377..93d0bb9 100644 --- a/pylintrc +++ b/pylintrc @@ -1,652 +1,10 @@ [MAIN] disable= E1101, # Dynamic attributes - C0301, # Line too long E0307, # __str__ does not return str W0223, # method is not overriden in child class W0613, # unused argument C0103, # snake case (unittests) W0511, # fixme - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Clear in-memory caches upon conclusion of linting. Useful if running pylint -# in a server-like mode. -clear-cache-post-run=no - -# Load and enable all available extensions. Use --list-extensions to see a list -# all available extensions. -#enable-all-extensions= - -# In error mode, messages with a category besides ERROR or FATAL are -# suppressed, and no reports are done by default. Error mode is compatible with -# disabling specific errors. -#errors-only= - -# Always return a 0 (non-error) status code, even if lint errors are found. -# This is primarily useful in continuous integration scripts. -#exit-zero= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-allow-list= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -extension-pkg-whitelist= - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -fail-on= - -# Specify a score threshold under which the program will exit with error. -fail-under=10 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -#from-stdin= - -# Files or directories to be skipped. They should be base names, not paths. -ignore=CVS - -# Add files or directories matching the regular expressions patterns to the -# ignore-list. The regex matches against paths and can be in Posix or Windows -# format. Because '\\' represents the directory delimiter on Windows systems, -# it can't be used as an escape character. -ignore-paths= - -# Files or directories matching the regular expression patterns are skipped. -# The regex matches against base names, not paths. The default value ignores -# Emacs file locks -ignore-patterns=^\.# - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use, and will cap the count on Windows to -# avoid hangs. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Minimum Python version to use for version dependent checks. Will default to -# the version used to run pylint. -py-version=3.10 - -# Discover python modules and packages in the file system subtree. -recursive=no - -# Add paths to the list of the source roots. Supports globbing patterns. The -# source root is an absolute path or a path relative to the current working -# directory used to determine a package namespace for modules located under the -# source root. -source-roots= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# In verbose mode, extra non-checker-related info will be displayed. -#verbose= - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. If left empty, argument names will be checked with the set -# naming style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -#class-attribute-rgx= - -# Naming style matching correct class constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -#class-const-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. If left empty, class names will be checked with the set naming style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. If left empty, function names will be checked with the set -# naming style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Regular expression matching correct type alias names. If left empty, type -# alias names will be checked with the set naming style. -#typealias-rgx= - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -#typevar-rgx= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. If left empty, variable names will be checked with the set -# naming style. -#variable-rgx= - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - asyncSetUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -exclude-too-few-public-methods= - -# List of qualified class names to ignore when counting class parents (see -# R0901) -ignored-parents= - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when caught. -overgeneral-exceptions=builtins.BaseException,builtins.Exception - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow explicit reexports by alias from a package __init__. -allow-reexport-from-package=no - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules= - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to the given file (report RP0402 must not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, -# UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - missing-module-docstring, - missing-class-docstring, - missing-function-docstring, - import-error, - wildcard-import, - import-error, - unused-wildcard-import, - too-few-public-methods, - invalid-name, - unused-variable, - import-outside-toplevel, - duplicate-code - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[METHOD_ARGS] - -# List of qualified names (i.e., library.method) which require a timeout -# parameter e.g. 'requests.api.get,requests.api.post' -timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -notes-rgx= - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each -# category, as well as 'statement' which is the total number of statements -# analyzed. This score is used by the global evaluation report (RP0004). -evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -#output-format= - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[SIMILARITIES] - -# Comments are removed from the similarity computation -ignore-comments=yes - -# Docstrings are removed from the similarity computation -ignore-docstrings=yes - -# Imports are removed from the similarity computation -ignore-imports=yes - -# Signatures are removed from the similarity computation -ignore-signatures=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. No available dictionaries : You need to install -# both the python package and the system dependency for enchant to work.. -spelling-dict= - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins=no-member, - not-async-context-manager, - not-context-manager, - attribute-defined-outside-init - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx=.*[Mm]ixin - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of names allowed to shadow builtins -allowed-redefined-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io +ignore-paths=.*/migrations From 2482cc76aab37d7408ab98faa47510895902e729 Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 20:41:55 +0400 Subject: [PATCH 04/11] make simple testcase --- demo/tests/tests_django.py | 35 ++++++++++++-- demo/tests/tests_dry.py | 95 ++++++++++++++++++++++++-------------- demo/urls.py | 1 + demo/views.py | 17 +++++++ dry_tests/__init__.py | 2 +- dry_tests/models.py | 3 ++ dry_tests/testcases.py | 63 +++++++++++++++++-------- 7 files changed, 158 insertions(+), 58 deletions(-) diff --git a/demo/tests/tests_django.py b/demo/tests/tests_django.py index a9d744c..8503bc9 100644 --- a/demo/tests/tests_django.py +++ b/demo/tests/tests_django.py @@ -1,13 +1,13 @@ """ Tests with django TestCase """ -from django.test import TestCase +from django.test import TestCase, SimpleTestCase from demo.models import Simple -class ViewTestCase(TestCase): +class ViewSimpleTestCase(SimpleTestCase): """ - Django Test Case to Test views + Django Test Case to Test views without database """ def setUp(self): @@ -59,6 +59,35 @@ def test_value_in_content(self): """ self.assertContains(self.client.get(self.url), 'Title', 1) + def test_query_params_and_kwargs(self): + """ + Send kwargs and query params to url + :return: + """ + url = '/query/kwarg_value/?a=x&b=y' + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + context = response.context + self.assertIn('a', context) + self.assertIn('b', context) + self.assertIn('kwarg', context) + self.assertEqual(context['a'], 'x') + self.assertEqual(context['b'], 'y') + self.assertEqual(context['kwarg'], 'kwarg_value') + + +class ViewTestCase(TestCase): + """ + Django tests with database + """ + + def setUp(self): + """ + Set Up test data + :return: + """ + self.url = '/' + def test_new_object_created(self): """ Test New Object Was Created diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index f4d055d..3fadd73 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -1,23 +1,16 @@ """ Tests with Django Dry Tests TestCase """ -from dry_tests import Request, Response, TestCase, POST +from dry_tests import Request, Response, SimpleTestCase, POST # from .db_test_data import create_simple -from demo.models import Simple +# from demo.models import Simple -class ViewTestCase(TestCase): +class ViewTestCase(SimpleTestCase): """ - Concrete TestCase inherited from DRY TestCase + Concrete TestCase inherited from DRY SimpleTestCase """ - def clear_db(self): - """ - Clear database method for subtests - :return: - """ - Simple.objects.all().delete() - def test_main(self): """ All asserts tests in one test function @@ -108,41 +101,73 @@ def test_main(self): 'should_fail': True, 'assert': self.assertContentValue, }, - # Create object + # url_args { 'request': Request( - url='/', - method=POST, - data={'name': 'new_name'} + url='/query/', + url_args=['kwarg_value'], ), 'response': Response( - created={ - 'model': Simple, - 'fields': { - 'name': 'new_name', - } - }, + context_values={'kwarg': 'kwarg_value'} ), 'should_fail': False, - 'assert': self.assertCreated, + 'assert': self.assertContextValues, }, + # url_params { 'request': Request( - url='/', - method=POST, - data={'name': 'new_name'} + url='/query/', + url_args=['kwarg_value'], + url_params={ + 'a': 'x', + 'b': 'y', + } ), 'response': Response( - created={ - 'model': Simple, - 'fields': { - 'name': 'error_name', - } - }, + context_values={ + 'kwarg': 'kwarg_value', + 'a': 'x', + 'b': 'y', + } ), - 'should_fail': True, - 'assert': self.assertCreated, - }, + 'should_fail': False, + 'assert': self.assertContextValues, + } + # Create object + # { + # 'request': Request( + # url='/', + # method=POST, + # data={'name': 'new_name'} + # ), + # 'response': Response( + # created={ + # 'model': Simple, + # 'fields': { + # 'name': 'new_name', + # } + # }, + # ), + # 'should_fail': False, + # 'assert': self.assertCreated, + # }, + # { + # 'request': Request( + # url='/', + # method=POST, + # data={'name': 'new_name'} + # ), + # 'response': Response( + # created={ + # 'model': Simple, + # 'fields': { + # 'name': 'error_name', + # } + # }, + # ), + # 'should_fail': True, + # 'assert': self.assertCreated, + # }, # Test Detail View # { # 'request': Request( @@ -174,6 +199,6 @@ def test_main(self): assert_function(item['request'], item['response']) else: assert_function(request, response) - self.clear_db() # TODO: someting wrong with supTests + # self.clear_db() # TODO: someting wrong with supTests # self.tearDown() # self.tearDownClass() diff --git a/demo/urls.py b/demo/urls.py index 27e15c8..ba856b6 100644 --- a/demo/urls.py +++ b/demo/urls.py @@ -9,4 +9,5 @@ urlpatterns = [ path('', views.index_view), path('/', views.one_view), + path('query//', views.param_view), ] diff --git a/demo/views.py b/demo/views.py index 173a30d..7e92798 100644 --- a/demo/views.py +++ b/demo/views.py @@ -24,6 +24,23 @@ def index_view(request): return HttpResponseRedirect('/') +def param_view(request, kwarg): + """ + view to test params and kwargs in url + :param reqeust: + :param kwarg: + :return: + """ + a_param = request.GET.get('a') + b_param = request.GET.get('b') + context = { + 'a': a_param, + 'b': b_param, + 'kwarg': kwarg, + } + return render(request, 'demo/index.html', context) + + def one_view(request, pk): """ One view with pk to detail, update and delete object diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index d8589ae..09bf460 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -1,5 +1,5 @@ """ Init file for dry_test package """ -from .testcases import TestCase +from .testcases import SimpleTestCase from .models import Request, Response, GET, POST diff --git a/dry_tests/models.py b/dry_tests/models.py index b7299ad..c014a42 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -15,6 +15,9 @@ class Request: Main Request Model """ url: str + # url_args: list = field(default_factory=lambda: []) + url_args: list = None + url_params: dict = None method: Literal[GET, POST] = GET data: dict = None diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index c9042ce..982f6a7 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -1,15 +1,42 @@ """ Base testcases """ -from django.test import TestCase as DjangoTestCase +from django.test import ( + # TestCase as DjangoTestCase, + SimpleTestCase as DjangoSimpleTestCase, +) from .models import GET, POST -class TestCase(DjangoTestCase): +class SimpleTestCase(DjangoSimpleTestCase): """ Main TestCase with test database """ + @staticmethod + def make_url(request): + """ + Make url with params and kwargs + :param request: + :return: + """ + url = request.url + # url_args + url_args_list = request.url_args + if url_args_list: + url_args = '/'.join(url_args_list) + url = f'{url}{url_args}/' + # url_params + url_params_dict = request.url_params + if url_params_dict: + url_params_list = [] + for key, value in url_params_dict.items(): + pair = f'{key}={value}' + url_params_list.append(pair) + url_params_str = '&'.join(url_params_list) + url = f'{url}?{url_params_str}' + return url + def get_url_response(self, request): """ get response with test client @@ -21,10 +48,8 @@ def get_url_response(self, request): POST: self.client.post } - # if request.data is None: - # url_response = requests[request.method](request.url) - # else: - url_response = requests[request.method](request.url, data=request.data) + url = self.make_url(request) + url_response = requests[request.method](url, data=request.data) return url_response @@ -83,16 +108,16 @@ def assertContentValue(self, request, response): url_response = self.get_url_response(request) self.assertContains(url_response, response.content_value) - def assertCreated(self, request, response): - """ - Check object was created - :param request: Request - :param response: Response - :return: None - """ - created = response.created - model = created['model'] - fields = created['fields'] - self.assertFalse(model.objects.filter(**fields).exists()) - self.get_url_response(request) - self.assertTrue(model.objects.filter(**fields).exists()) + # def assertCreated(self, request, response): + # """ + # Check object was created + # :param request: Request + # :param response: Response + # :return: None + # """ + # created = response.created + # model = created['model'] + # fields = created['fields'] + # self.assertFalse(model.objects.filter(**fields).exists()) + # self.get_url_response(request) + # self.assertTrue(model.objects.filter(**fields).exists()) From f683abf757a257bbefb80f5acf35fcf2d88b68b3 Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 21:11:28 +0400 Subject: [PATCH 05/11] add assertDRY --- demo/tests/tests_dry.py | 95 +++++++++++++++++++++++++++++++++++++++++ dry_tests/testcases.py | 19 +++++++++ 2 files changed, 114 insertions(+) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 3fadd73..4ca6932 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -17,6 +17,29 @@ def test_main(self): :return: """ data = [ + # Multy parameters GET + # DONE + { + 'request': Request(url='/'), + 'response': Response( + status_code=200, + in_context='title', + context_values={'title': 'Title'}, + content_value='Title', + ), + 'should_fail': False, + 'assert': self.assertDRY, + }, + # Multy parameters POST + { + 'request': Request(url='/', method=POST), + 'response': Response( + status_code=302, + redirect_url='/', + ), + 'should_fail': False, + 'assert': self.assertDRY, + }, # RedirectUrl { 'request': Request(url='/', method=POST), @@ -30,6 +53,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertRedirectUrl, }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302, redirect_url='/'), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302, redirect_url='/fail_redirect/'), + 'should_fail': True, + 'assert': self.assertDRY, + }, # Post StatusCode { 'request': Request(url='/', method=POST), @@ -43,6 +78,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertStatusCode, }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=302), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/', method=POST), + 'response': Response(status_code=404), + 'should_fail': True, + 'assert': self.assertDRY, + }, # Get StatusCode { 'request' : Request(url='/'), @@ -56,6 +103,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertStatusCode, }, + { + 'request': Request(url='/'), + 'response': Response(status_code=200), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/'), + 'response': Response(status_code=404), + 'should_fail': True, + 'assert': self.assertDRY, + }, # Value in Context { 'request': Request(url='/'), @@ -69,6 +128,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertValueInContext, }, + { + 'request': Request(url='/'), + 'response': Response(in_context='title'), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/'), + 'response': Response(in_context='not_in_context_key'), + 'should_fail': True, + 'assert': self.assertDRY, + }, # Context Value { 'request': Request(url='/'), @@ -88,6 +159,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertContextValues, }, + { + 'request': Request(url='/'), + 'response': Response(context_values={'title': 'Title'}), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/'), + 'response': Response(context_values={'title': 'Error value'}), + 'should_fail': True, + 'assert': self.assertDRY, + }, # Content Value { 'request': Request(url='/'), @@ -101,6 +184,18 @@ def test_main(self): 'should_fail': True, 'assert': self.assertContentValue, }, + { + 'request': Request(url='/'), + 'response': Response(content_value='Title'), + 'should_fail': False, + 'assert': self.assertDRY, + }, + { + 'request': Request(url='/'), + 'response': Response(content_value='Error value'), + 'should_fail': True, + 'assert': self.assertDRY, + }, # url_args { 'request': Request( diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index 982f6a7..f294e5d 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -108,6 +108,25 @@ def assertContentValue(self, request, response): url_response = self.get_url_response(request) self.assertContains(url_response, response.content_value) + def assertDRY(self, request, response): + """ + Main assert for request and response + Check all parameters sended in response + :param request: Request + :param response: Response + :return: None + """ + if response.status_code: + self.assertStatusCode(request, response) + if response.redirect_url: + self.assertRedirectUrl(request, response) + if response.in_context: + self.assertValueInContext(request, response) + if response.context_values: + self.assertContextValues(request, response) + if response.content_value: + self.assertContentValue(request, response) + # def assertCreated(self, request, response): # """ # Check object was created From de39e029685ae7eef3724bf5a50c0a6b50c9ddaf Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 21:51:28 +0400 Subject: [PATCH 06/11] change asserts interface --- demo/tests/tests_dry.py | 20 +++++----- dry_tests/__init__.py | 2 +- dry_tests/models.py | 42 ++++++++++++++++++++- dry_tests/testcases.py | 82 +++++++++-------------------------------- 4 files changed, 69 insertions(+), 77 deletions(-) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 4ca6932..588f3bd 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -1,7 +1,12 @@ """ Tests with Django Dry Tests TestCase """ -from dry_tests import Request, Response, SimpleTestCase, POST +from dry_tests import ( + Request, + ExpectedResponse as Response, + SimpleTestCase, + POST +) # from .db_test_data import create_simple # from demo.models import Simple @@ -282,18 +287,13 @@ def test_main(self): for item in data: request = item['request'] - response = item['response'] + tru_response = request.get_url_response(self.client) + expected_response = item['response'] assert_function = item['assert'] with self.subTest(msg=str(item)): - # self.setUp() - # self.setUpClass() - # self.setUpTestData() - # self.clear_db() if item['should_fail']: with self.assertRaises(AssertionError): - assert_function(item['request'], item['response']) + assert_function(tru_response, expected_response) else: - assert_function(request, response) + assert_function(tru_response, expected_response) # self.clear_db() # TODO: someting wrong with supTests - # self.tearDown() - # self.tearDownClass() diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index 09bf460..14cef14 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -2,4 +2,4 @@ Init file for dry_test package """ from .testcases import SimpleTestCase -from .models import Request, Response, GET, POST +from .models import Request, ExpectedResponse, GET, POST diff --git a/dry_tests/models.py b/dry_tests/models.py index c014a42..a659e7a 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -15,15 +15,53 @@ class Request: Main Request Model """ url: str - # url_args: list = field(default_factory=lambda: []) url_args: list = None url_params: dict = None method: Literal[GET, POST] = GET data: dict = None + def make_url(self): + """ + Make url with params and kwargs + :param request: + :return: + """ + url = self.url + # url_args + url_args_list = self.url_args + if url_args_list: + url_args = '/'.join(url_args_list) + url = f'{url}{url_args}/' + # url_params + url_params_dict = self.url_params + if url_params_dict: + url_params_list = [] + for key, value in url_params_dict.items(): + pair = f'{key}={value}' + url_params_list.append(pair) + url_params_str = '&'.join(url_params_list) + url = f'{url}?{url_params_str}' + return url + + def get_url_response(self, client): + """ + get response with test client + :param client: Request client + :return: client response + """ + requests = { + GET: client.get, + POST: client.post + } + + url = self.make_url() + url_response = requests[self.method](url, data=self.data) + + return url_response + @dataclass(frozen=True) -class Response: +class ExpectedResponse: """ Main Excepted Response Model """ diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index f294e5d..adf21b7 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -5,99 +5,54 @@ # TestCase as DjangoTestCase, SimpleTestCase as DjangoSimpleTestCase, ) -from .models import GET, POST class SimpleTestCase(DjangoSimpleTestCase): """ - Main TestCase with test database + Main TestCase without test database """ - @staticmethod - def make_url(request): - """ - Make url with params and kwargs - :param request: - :return: - """ - url = request.url - # url_args - url_args_list = request.url_args - if url_args_list: - url_args = '/'.join(url_args_list) - url = f'{url}{url_args}/' - # url_params - url_params_dict = request.url_params - if url_params_dict: - url_params_list = [] - for key, value in url_params_dict.items(): - pair = f'{key}={value}' - url_params_list.append(pair) - url_params_str = '&'.join(url_params_list) - url = f'{url}?{url_params_str}' - return url - - def get_url_response(self, request): - """ - get response with test client - :param request: Request model - :return: client response - """ - requests = { - GET: self.client.get, - POST: self.client.post - } - - url = self.make_url(request) - url_response = requests[request.method](url, data=request.data) - - return url_response - - def assertStatusCode(self, request, response): + def assertStatusCode(self, true_response, response): """ Check status code :param request: Request :param response: Response :return: None """ - url_response = self.get_url_response(request) - self.assertEqual(url_response.status_code, response.status_code) + self.assertEqual(true_response.status_code, response.status_code) - def assertRedirectUrl(self, request, response): + def assertRedirectUrl(self, true_response, response): """ Check Redirect Url :param request: Request :param response: Response :return: None """ - url_response = self.get_url_response(request) - self.assertRedirects(url_response, response.redirect_url) + self.assertRedirects(true_response, response.redirect_url) - def assertValueInContext(self, request, response): + def assertValueInContext(self, true_response, response): """ Check Value In Context :param request: Request :param response: Response :return: None """ - url_response = self.get_url_response(request) - self.assertIn(response.in_context, url_response.context) + self.assertIn(response.in_context, true_response.context) - def assertContextValues(self, request, response): + def assertContextValues(self, true_response, response): """ Check Context Value :param request: Request :param response: Response :return: None """ - url_response = self.get_url_response(request) - context = url_response.context + context = true_response.context context_values = response.context_values for key, value in context_values.items(): self.assertIn(key, context) self.assertEqual(value, context[key]) - def assertContentValue(self, request, response): + def assertContentValue(self, true_response, response): """ Check Content Value :param request: Request @@ -105,27 +60,26 @@ def assertContentValue(self, request, response): :return: None """ # TODO: Response send every time - it's not good - url_response = self.get_url_response(request) - self.assertContains(url_response, response.content_value) + self.assertContains(true_response, response.content_value) - def assertDRY(self, request, response): + def assertDRY(self, true_response, response): """ Main assert for request and response - Check all parameters sended in response + Check all parameters sent in response :param request: Request :param response: Response :return: None """ if response.status_code: - self.assertStatusCode(request, response) + self.assertStatusCode(true_response, response) if response.redirect_url: - self.assertRedirectUrl(request, response) + self.assertRedirectUrl(true_response, response) if response.in_context: - self.assertValueInContext(request, response) + self.assertValueInContext(true_response, response) if response.context_values: - self.assertContextValues(request, response) + self.assertContextValues(true_response, response) if response.content_value: - self.assertContentValue(request, response) + self.assertContentValue(true_response, response) # def assertCreated(self, request, response): # """ From 53b8ff829ac1c7522b2ab438793eb6eb6f657e03 Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 22:47:35 +0400 Subject: [PATCH 07/11] add content value --- demo/tests/tests_dry.py | 49 ++++++++++++++++++--------- dry_tests/__init__.py | 2 +- dry_tests/models.py | 74 +++++++++++++++++++++++++++++++++++------ dry_tests/testcases.py | 10 +++--- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 588f3bd..52f5a5f 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -5,7 +5,9 @@ Request, ExpectedResponse as Response, SimpleTestCase, - POST + POST, + Url, + ContentValue, ) # from .db_test_data import create_simple # from demo.models import Simple @@ -30,7 +32,7 @@ def test_main(self): status_code=200, in_context='title', context_values={'title': 'Title'}, - content_value='Title', + content_values=['Title'], ), 'should_fail': False, 'assert': self.assertDRY, @@ -179,33 +181,46 @@ def test_main(self): # Content Value { 'request': Request(url='/'), - 'response': Response(content_value='Title'), + 'response': Response(content_values=['Title']), 'should_fail': False, - 'assert': self.assertContentValue, + 'assert': self.assertContentValues, }, { 'request': Request(url='/'), - 'response': Response(content_value='Error value'), + 'response': Response(content_values=[ + ContentValue( + value='Title', + count=1 + ) + ]), + 'should_fail': False, + 'assert': self.assertContentValues, + }, + { + 'request': Request(url='/'), + 'response': Response(content_values=['Error value']), 'should_fail': True, - 'assert': self.assertContentValue, + 'assert': self.assertContentValues, }, { 'request': Request(url='/'), - 'response': Response(content_value='Title'), + 'response': Response(content_values=['Title']), 'should_fail': False, 'assert': self.assertDRY, }, { 'request': Request(url='/'), - 'response': Response(content_value='Error value'), + 'response': Response(content_values=['Error value']), 'should_fail': True, 'assert': self.assertDRY, }, # url_args { 'request': Request( - url='/query/', - url_args=['kwarg_value'], + url=Url( + url='/query/', + args=['kwarg_value'] + ) ), 'response': Response( context_values={'kwarg': 'kwarg_value'} @@ -216,12 +231,14 @@ def test_main(self): # url_params { 'request': Request( - url='/query/', - url_args=['kwarg_value'], - url_params={ - 'a': 'x', - 'b': 'y', - } + url = Url( + url='/query/', + args=['kwarg_value'], + params={ + 'a': 'x', + 'b': 'y', + } + ) ), 'response': Response( context_values={ diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index 14cef14..8f19ffd 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -2,4 +2,4 @@ Init file for dry_test package """ from .testcases import SimpleTestCase -from .models import Request, ExpectedResponse, GET, POST +from .models import Request, ExpectedResponse, GET, POST, Url, ContentValue diff --git a/dry_tests/models.py b/dry_tests/models.py index a659e7a..117c140 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -10,15 +10,13 @@ @dataclass(frozen=True) -class Request: +class Url: """ - Main Request Model + Url for Request with args and params """ url: str - url_args: list = None - url_params: dict = None - method: Literal[GET, POST] = GET - data: dict = None + args: list = None + params: dict = None def make_url(self): """ @@ -28,12 +26,12 @@ def make_url(self): """ url = self.url # url_args - url_args_list = self.url_args + url_args_list = self.args if url_args_list: url_args = '/'.join(url_args_list) url = f'{url}{url_args}/' # url_params - url_params_dict = self.url_params + url_params_dict = self.params if url_params_dict: url_params_list = [] for key, value in url_params_dict.items(): @@ -43,6 +41,41 @@ def make_url(self): url = f'{url}?{url_params_str}' return url + +@dataclass(frozen=True) +class Request: + """ + Main Request Model + """ + url: str | Url + # url_args: list = None + # url_params: dict = None + method: Literal[GET, POST] = GET + data: dict = None + + # def make_url(self): + # """ + # Make url with params and kwargs + # :param request: + # :return: + # """ + # url = self.url + # # url_args + # url_args_list = self.url_args + # if url_args_list: + # url_args = '/'.join(url_args_list) + # url = f'{url}{url_args}/' + # # url_params + # url_params_dict = self.url_params + # if url_params_dict: + # url_params_list = [] + # for key, value in url_params_dict.items(): + # pair = f'{key}={value}' + # url_params_list.append(pair) + # url_params_str = '&'.join(url_params_list) + # url = f'{url}?{url_params_str}' + # return url + def get_url_response(self, client): """ get response with test client @@ -54,12 +87,21 @@ def get_url_response(self, client): POST: client.post } - url = self.make_url() + url = self.url.make_url() if isinstance(self.url, Url) else self.url url_response = requests[self.method](url, data=self.data) return url_response +@dataclass(frozen=True) +class ContentValue: + """ + Content Value Dataclass + """ + value: str + count: int = None + + @dataclass(frozen=True) class ExpectedResponse: """ @@ -69,6 +111,18 @@ class ExpectedResponse: redirect_url: str = None in_context: str = None context_values: dict = None - content_value: str = None + content_values: list = None created: Model = None # db_data: callable = None + + def get_content_values(self): + """ + Convert content values to ContentValue + :return: + """ + return [ + content_value + if isinstance(content_value, ContentValue) + else ContentValue(value=content_value) + for content_value in self.content_values + ] diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index adf21b7..f1a584f 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -52,15 +52,15 @@ def assertContextValues(self, true_response, response): self.assertIn(key, context) self.assertEqual(value, context[key]) - def assertContentValue(self, true_response, response): + def assertContentValues(self, true_response, response): """ Check Content Value :param request: Request :param response: Response :return: None """ - # TODO: Response send every time - it's not good - self.assertContains(true_response, response.content_value) + for content_value in response.get_content_values(): + self.assertContains(true_response, content_value.value, content_value.count) def assertDRY(self, true_response, response): """ @@ -78,8 +78,8 @@ def assertDRY(self, true_response, response): self.assertValueInContext(true_response, response) if response.context_values: self.assertContextValues(true_response, response) - if response.content_value: - self.assertContentValue(true_response, response) + if response.content_values: + self.assertContentValues(true_response, response) # def assertCreated(self, request, response): # """ From 9da3ece949342911c30fc79be08a5fffa9cc540d Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 23:47:10 +0400 Subject: [PATCH 08/11] try response pair --- demo/tests/tests_dry.py | 89 ++++++++++++++++++++++++++++++++++------- dry_tests/__init__.py | 2 +- dry_tests/models.py | 32 ++++----------- dry_tests/testcases.py | 53 +++++++++++++----------- 4 files changed, 113 insertions(+), 63 deletions(-) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 52f5a5f..1020dbf 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -3,11 +3,12 @@ """ from dry_tests import ( Request, - ExpectedResponse as Response, + TrueResponse as Response, SimpleTestCase, POST, Url, ContentValue, + ResponsePair, ) # from .db_test_data import create_simple # from demo.models import Simple @@ -18,6 +19,64 @@ class ViewTestCase(SimpleTestCase): Concrete TestCase inherited from DRY SimpleTestCase """ + # def test_many(self): + # data = [ + # { + # 'request': Request(url='/').get_url_response(self.client), + # 'response': Response( + # status_code=200, + # in_context='title', + # context_values={'title': 'Title'}, + # content_values=['Title'], + # ), + # }, + # # Multy parameters POST + # { + # 'request': Request(url='/', method=POST).get_url_response(self.client), + # 'response': Response( + # status_code=302, + # redirect_url='/', + # ), + # }, + # ] + # + # self.assertManyExpectedResponses(data) + + def test_response_pair(self): + response_pair = ResponsePair( + current_response=Request(url='/').get_url_response(self.client), + true_response=Response( + status_code=200, + in_context='title', + context_values={'title': 'Title'}, + content_values=['Title'], + ), + ) + self.assertResponseIsTrue(response_pair) + + def test_many_pairs(self): + response_pairs = [ + ResponsePair( + current_response=Request(url='/').get_url_response(self.client), + true_response=Response( + status_code=200, + in_context='title', + context_values={'title': 'Title'}, + content_values=['Title'], + ), + ), + ResponsePair( + current_response=Request(url='/').get_url_response(self.client), + true_response=Response( + status_code=200, + in_context='title', + context_values={'title': 'Title'}, + content_values=['Title'], + ), + ), + ] + self.assertResponsesAreTrue(response_pairs) + def test_main(self): """ All asserts tests in one test function @@ -35,7 +94,7 @@ def test_main(self): content_values=['Title'], ), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Multy parameters POST { @@ -45,7 +104,7 @@ def test_main(self): redirect_url='/', ), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # RedirectUrl { @@ -64,13 +123,13 @@ def test_main(self): 'request': Request(url='/', method=POST), 'response': Response(status_code=302, redirect_url='/'), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/', method=POST), 'response': Response(status_code=302, redirect_url='/fail_redirect/'), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Post StatusCode { @@ -89,13 +148,13 @@ def test_main(self): 'request': Request(url='/', method=POST), 'response': Response(status_code=302), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/', method=POST), 'response': Response(status_code=404), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Get StatusCode { @@ -114,13 +173,13 @@ def test_main(self): 'request': Request(url='/'), 'response': Response(status_code=200), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/'), 'response': Response(status_code=404), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Value in Context { @@ -139,13 +198,13 @@ def test_main(self): 'request': Request(url='/'), 'response': Response(in_context='title'), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/'), 'response': Response(in_context='not_in_context_key'), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Context Value { @@ -170,13 +229,13 @@ def test_main(self): 'request': Request(url='/'), 'response': Response(context_values={'title': 'Title'}), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/'), 'response': Response(context_values={'title': 'Error value'}), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # Content Value { @@ -206,13 +265,13 @@ def test_main(self): 'request': Request(url='/'), 'response': Response(content_values=['Title']), 'should_fail': False, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, { 'request': Request(url='/'), 'response': Response(content_values=['Error value']), 'should_fail': True, - 'assert': self.assertDRY, + 'assert': self.assertTrueResponse, }, # url_args { diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index 8f19ffd..226e4b2 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -2,4 +2,4 @@ Init file for dry_test package """ from .testcases import SimpleTestCase -from .models import Request, ExpectedResponse, GET, POST, Url, ContentValue +from .models import Request, TrueResponse, GET, POST, Url, ContentValue, ResponsePair diff --git a/dry_tests/models.py b/dry_tests/models.py index 117c140..d8e2300 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Literal from django.db.models import Model +from django.http import HttpRequest GET = 'get' POST = 'post' @@ -53,29 +54,6 @@ class Request: method: Literal[GET, POST] = GET data: dict = None - # def make_url(self): - # """ - # Make url with params and kwargs - # :param request: - # :return: - # """ - # url = self.url - # # url_args - # url_args_list = self.url_args - # if url_args_list: - # url_args = '/'.join(url_args_list) - # url = f'{url}{url_args}/' - # # url_params - # url_params_dict = self.url_params - # if url_params_dict: - # url_params_list = [] - # for key, value in url_params_dict.items(): - # pair = f'{key}={value}' - # url_params_list.append(pair) - # url_params_str = '&'.join(url_params_list) - # url = f'{url}?{url_params_str}' - # return url - def get_url_response(self, client): """ get response with test client @@ -103,7 +81,7 @@ class ContentValue: @dataclass(frozen=True) -class ExpectedResponse: +class TrueResponse: """ Main Excepted Response Model """ @@ -126,3 +104,9 @@ def get_content_values(self): else ContentValue(value=content_value) for content_value in self.content_values ] + + +@dataclass(frozen=True) +class ResponsePair: + current_response: HttpRequest + true_response: TrueResponse \ No newline at end of file diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index f1a584f..ef3b7ec 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -12,57 +12,57 @@ class SimpleTestCase(DjangoSimpleTestCase): Main TestCase without test database """ - def assertStatusCode(self, true_response, response): + def assertStatusCode(self, current_response, true_response): """ Check status code :param request: Request :param response: Response :return: None """ - self.assertEqual(true_response.status_code, response.status_code) + self.assertEqual(current_response.status_code, true_response.status_code) - def assertRedirectUrl(self, true_response, response): + def assertRedirectUrl(self, current_response, true_response): """ Check Redirect Url :param request: Request :param response: Response :return: None """ - self.assertRedirects(true_response, response.redirect_url) + self.assertRedirects(current_response, true_response.redirect_url) - def assertValueInContext(self, true_response, response): + def assertValueInContext(self, current_response, true_response): """ Check Value In Context :param request: Request :param response: Response :return: None """ - self.assertIn(response.in_context, true_response.context) + self.assertIn(true_response.in_context, current_response.context) - def assertContextValues(self, true_response, response): + def assertContextValues(self, current_response, true_response): """ Check Context Value :param request: Request :param response: Response :return: None """ - context = true_response.context - context_values = response.context_values + context = current_response.context + context_values = true_response.context_values for key, value in context_values.items(): self.assertIn(key, context) self.assertEqual(value, context[key]) - def assertContentValues(self, true_response, response): + def assertContentValues(self, current_response, true_response): """ Check Content Value :param request: Request :param response: Response :return: None """ - for content_value in response.get_content_values(): - self.assertContains(true_response, content_value.value, content_value.count) + for content_value in true_response.get_content_values(): + self.assertContains(current_response, content_value.value, content_value.count) - def assertDRY(self, true_response, response): + def assertTrueResponse(self, current_response, true_response): """ Main assert for request and response Check all parameters sent in response @@ -70,16 +70,23 @@ def assertDRY(self, true_response, response): :param response: Response :return: None """ - if response.status_code: - self.assertStatusCode(true_response, response) - if response.redirect_url: - self.assertRedirectUrl(true_response, response) - if response.in_context: - self.assertValueInContext(true_response, response) - if response.context_values: - self.assertContextValues(true_response, response) - if response.content_values: - self.assertContentValues(true_response, response) + if true_response.status_code: + self.assertStatusCode(current_response, true_response) + if true_response.redirect_url: + self.assertRedirectUrl(current_response, true_response) + if true_response.in_context: + self.assertValueInContext(current_response, true_response) + if true_response.context_values: + self.assertContextValues(current_response, true_response) + if true_response.content_values: + self.assertContentValues(current_response, true_response) + + def assertResponseIsTrue(self, response_pair): + self.assertTrueResponse(response_pair.current_response, response_pair.true_response) + + def assertResponsesAreTrue(self, response_pairs): + for response_pair in response_pairs: + self.assertResponseIsTrue(response_pair) # def assertCreated(self, request, response): # """ From d786e936e3f052c2d509ec126f50b0013362e18d Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Wed, 11 Oct 2023 23:55:25 +0400 Subject: [PATCH 09/11] add class to pair --- demo/tests/tests_dry.py | 31 ++++++--------------- dry_tests/models.py | 60 +++++++++++++++++++++-------------------- dry_tests/testcases.py | 10 +++++++ 3 files changed, 49 insertions(+), 52 deletions(-) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 1020dbf..29ddc20 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -19,30 +19,11 @@ class ViewTestCase(SimpleTestCase): Concrete TestCase inherited from DRY SimpleTestCase """ - # def test_many(self): - # data = [ - # { - # 'request': Request(url='/').get_url_response(self.client), - # 'response': Response( - # status_code=200, - # in_context='title', - # context_values={'title': 'Title'}, - # content_values=['Title'], - # ), - # }, - # # Multy parameters POST - # { - # 'request': Request(url='/', method=POST).get_url_response(self.client), - # 'response': Response( - # status_code=302, - # redirect_url='/', - # ), - # }, - # ] - # - # self.assertManyExpectedResponses(data) - def test_response_pair(self): + """ + Try to check the pair + :return: + """ response_pair = ResponsePair( current_response=Request(url='/').get_url_response(self.client), true_response=Response( @@ -55,6 +36,10 @@ def test_response_pair(self): self.assertResponseIsTrue(response_pair) def test_many_pairs(self): + """ + Try check many pairs + :return: + """ response_pairs = [ ResponsePair( current_response=Request(url='/').get_url_response(self.client), diff --git a/dry_tests/models.py b/dry_tests/models.py index d8e2300..266ca71 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -43,34 +43,6 @@ def make_url(self): return url -@dataclass(frozen=True) -class Request: - """ - Main Request Model - """ - url: str | Url - # url_args: list = None - # url_params: dict = None - method: Literal[GET, POST] = GET - data: dict = None - - def get_url_response(self, client): - """ - get response with test client - :param client: Request client - :return: client response - """ - requests = { - GET: client.get, - POST: client.post - } - - url = self.url.make_url() if isinstance(self.url, Url) else self.url - url_response = requests[self.method](url, data=self.data) - - return url_response - - @dataclass(frozen=True) class ContentValue: """ @@ -106,7 +78,37 @@ def get_content_values(self): ] +@dataclass(frozen=True) +class Request: + """ + Main Request Model + """ + # true_response: TrueResponse + url: str | Url + method: Literal[GET, POST] = GET + data: dict = None + + def get_url_response(self, client): + """ + get response with test client + :param client: Request client + :return: client response + """ + requests = { + GET: client.get, + POST: client.post + } + + url = self.url.make_url() if isinstance(self.url, Url) else self.url + url_response = requests[self.method](url, data=self.data) + + return url_response + + @dataclass(frozen=True) class ResponsePair: + """ + Current Response + TrueResponse + """ current_response: HttpRequest - true_response: TrueResponse \ No newline at end of file + true_response: TrueResponse diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index ef3b7ec..228d910 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -82,9 +82,19 @@ def assertTrueResponse(self, current_response, true_response): self.assertContentValues(current_response, true_response) def assertResponseIsTrue(self, response_pair): + """ + Check one response pair + :param response_pair: + :return: + """ self.assertTrueResponse(response_pair.current_response, response_pair.true_response) def assertResponsesAreTrue(self, response_pairs): + """ + Check all response pairs + :param response_pairs: + :return: + """ for response_pair in response_pairs: self.assertResponseIsTrue(response_pair) From 0709029359d19c63fb24d7e02a091c3cdac7e7dd Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Thu, 12 Oct 2023 12:47:47 +0400 Subject: [PATCH 10/11] remove response pair --- demo/tests/tests_dry.py | 32 ++++++++------------------------ dry_tests/__init__.py | 2 +- dry_tests/models.py | 10 ---------- dry_tests/testcases.py | 12 ++---------- 4 files changed, 11 insertions(+), 45 deletions(-) diff --git a/demo/tests/tests_dry.py b/demo/tests/tests_dry.py index 29ddc20..d8070f9 100644 --- a/demo/tests/tests_dry.py +++ b/demo/tests/tests_dry.py @@ -8,7 +8,6 @@ POST, Url, ContentValue, - ResponsePair, ) # from .db_test_data import create_simple # from demo.models import Simple @@ -19,40 +18,25 @@ class ViewTestCase(SimpleTestCase): Concrete TestCase inherited from DRY SimpleTestCase """ - def test_response_pair(self): - """ - Try to check the pair - :return: - """ - response_pair = ResponsePair( - current_response=Request(url='/').get_url_response(self.client), - true_response=Response( - status_code=200, - in_context='title', - context_values={'title': 'Title'}, - content_values=['Title'], - ), - ) - self.assertResponseIsTrue(response_pair) - - def test_many_pairs(self): + def test_many(self): """ Try check many pairs :return: """ response_pairs = [ - ResponsePair( - current_response=Request(url='/').get_url_response(self.client), - true_response=Response( + ( + Request(url='/').get_url_response(self.client), + Response( + status_code=200, in_context='title', context_values={'title': 'Title'}, content_values=['Title'], ), ), - ResponsePair( - current_response=Request(url='/').get_url_response(self.client), - true_response=Response( + ( + Request(url='/').get_url_response(self.client), + Response( status_code=200, in_context='title', context_values={'title': 'Title'}, diff --git a/dry_tests/__init__.py b/dry_tests/__init__.py index 226e4b2..9dc5f9b 100644 --- a/dry_tests/__init__.py +++ b/dry_tests/__init__.py @@ -2,4 +2,4 @@ Init file for dry_test package """ from .testcases import SimpleTestCase -from .models import Request, TrueResponse, GET, POST, Url, ContentValue, ResponsePair +from .models import Request, TrueResponse, GET, POST, Url, ContentValue diff --git a/dry_tests/models.py b/dry_tests/models.py index 266ca71..57c93a2 100644 --- a/dry_tests/models.py +++ b/dry_tests/models.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from typing import Literal from django.db.models import Model -from django.http import HttpRequest GET = 'get' POST = 'post' @@ -103,12 +102,3 @@ def get_url_response(self, client): url_response = requests[self.method](url, data=self.data) return url_response - - -@dataclass(frozen=True) -class ResponsePair: - """ - Current Response + TrueResponse - """ - current_response: HttpRequest - true_response: TrueResponse diff --git a/dry_tests/testcases.py b/dry_tests/testcases.py index 228d910..96c9669 100644 --- a/dry_tests/testcases.py +++ b/dry_tests/testcases.py @@ -81,22 +81,14 @@ def assertTrueResponse(self, current_response, true_response): if true_response.content_values: self.assertContentValues(current_response, true_response) - def assertResponseIsTrue(self, response_pair): - """ - Check one response pair - :param response_pair: - :return: - """ - self.assertTrueResponse(response_pair.current_response, response_pair.true_response) - def assertResponsesAreTrue(self, response_pairs): """ Check all response pairs :param response_pairs: :return: """ - for response_pair in response_pairs: - self.assertResponseIsTrue(response_pair) + for current_response, true_response in response_pairs: + self.assertTrueResponse(current_response, true_response) # def assertCreated(self, request, response): # """ From 0cf91dad9354e1a044b68145816df57c2057c34e Mon Sep 17 00:00:00 2001 From: quillcraftsman Date: Thu, 12 Oct 2023 22:43:35 +0400 Subject: [PATCH 11/11] add documentation --- CHECKLIST.md | 31 ++++---- DEVELOPER_DOCUMENTATION.md | 43 +++++++++++ README.md | 143 ++++++++++++++++++++++++++++++++++++- setup.py | 4 +- 4 files changed, 205 insertions(+), 16 deletions(-) create mode 100644 DEVELOPER_DOCUMENTATION.md diff --git a/CHECKLIST.md b/CHECKLIST.md index 5b3c471..1f2ed3b 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,26 +1,31 @@ [Main points](https://github.com/quillcraftsman/open-source-checklist#main-points) - [x] [Open Source Project Checklist](https://github.com/quillcraftsman/open-source-checklist#open-source-project-checklist) -- [X] [Existing Analogues](https://github.com/quillcraftsman/open-source-checklist#existing-analogues) +- [x] [Existing Analogues](https://github.com/quillcraftsman/open-source-checklist#existing-analogues) > [django-test-plus](https://github.com/revsys/django-test-plus/) -- [X] [Good Project Name](https://github.com/quillcraftsman/open-source-checklist#good-project-name) -- [ ] [Mission](https://github.com/quillcraftsman/open-source-checklist#mission) -- [ ] [State What the Project Is Free](https://github.com/quillcraftsman/open-source-checklist#state-what-the-project-is-free) -- [ ] [Features](https://github.com/quillcraftsman/open-source-checklist#features) -- [ ] [Requirements](https://github.com/quillcraftsman/open-source-checklist#requirements) -- [ ] [Development Status](https://github.com/quillcraftsman/open-source-checklist#development-status) -- [ ] [Download Page](https://github.com/quillcraftsman/open-source-checklist#download-page) +- [x] [Good Project Name](https://github.com/quillcraftsman/open-source-checklist#good-project-name) +- [x] [Mission](https://github.com/quillcraftsman/open-source-checklist#mission) +> https://github.com/quillcraftsman/django-dry-tests#mission +- [x] [State What the Project Is Free](https://github.com/quillcraftsman/open-source-checklist#state-what-the-project-is-free) +> https://github.com/quillcraftsman/django-dry-tests#open-source-project +- [x] [Features](https://github.com/quillcraftsman/open-source-checklist#features) +> https://github.com/quillcraftsman/django-dry-tests#features +- [x] [Requirements](https://github.com/quillcraftsman/open-source-checklist#requirements) +> https://github.com/quillcraftsman/django-dry-tests#requirements +- [x] [Development Status](https://github.com/quillcraftsman/open-source-checklist#development-status) +- [x] [Download Page](https://github.com/quillcraftsman/open-source-checklist#download-page) +> https://github.com/quillcraftsman/django-dry-tests#install - [X] [Version Control Access](https://github.com/quillcraftsman/open-source-checklist#version-control-access) - [X] [Bug Tracker Access](https://github.com/quillcraftsman/open-source-checklist#bug-tracker-access) -- [ ] [Communication Channels](https://github.com/quillcraftsman/open-source-checklist#communication-channels) -- - [ ] Discussions +- [x] [Communication Channels](https://github.com/quillcraftsman/open-source-checklist#communication-channels) +- - [x] Discussions - - [ ] Mailing List - - [ ] Real-time chat - - [ ] Forum - [X] [Developer Guidelines](https://github.com/quillcraftsman/open-source-checklist#developer-guidelines) > [CONTRIBUTING.md](CONTRIBUTING.md) -- [ ] [Documentation](https://github.com/quillcraftsman/open-source-checklist#documentation) -- [ ] [Developer Documentation](https://github.com/quillcraftsman/open-source-checklist#developer-documentation) -- [ ] [Availability of Documentation](https://github.com/quillcraftsman/open-source-checklist#availability-of-documentation) +- [x] [Documentation](https://github.com/quillcraftsman/open-source-checklist#documentation) +- [x] [Developer Documentation](https://github.com/quillcraftsman/open-source-checklist#developer-documentation) +- [x] [Availability of Documentation](https://github.com/quillcraftsman/open-source-checklist#availability-of-documentation) - [ ] [FAQ](https://github.com/quillcraftsman/open-source-checklist#faq) - [ ] [Examples Output and Screenshots](https://github.com/quillcraftsman/open-source-checklist#examples-output-and-screenshots) - [X] [License](https://github.com/quillcraftsman/open-source-checklist#license) diff --git a/DEVELOPER_DOCUMENTATION.md b/DEVELOPER_DOCUMENTATION.md new file mode 100644 index 0000000..46595fd --- /dev/null +++ b/DEVELOPER_DOCUMENTATION.md @@ -0,0 +1,43 @@ +# Django DRY Tests Developer Documentation + +## Makefile + +First check **Makefile**. It contains many useful commands to work with project + +## Main commands + +### Run Tests + +```commandline +make test +``` + +### Tests coverage + +```commandline +make coverage +``` + +### Lint + +```commandline +make lint +``` + +## How to develop new feature + +### Preparation + +- Make django test. Let it fail +- Add example view or something else with demo project +- Make the django test work. + +This step allow you to comfortably create new feature + +### Make new feature + +- Add new feature to the dry_tests package +- Make all tests work +- Check the coverage +- Run linter +- Make a new pull-request to contribute diff --git a/README.md b/README.md index 3a66264..5599baf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,144 @@ # Django DRY Tests -Some description will be here \ No newline at end of file +Package with new powerful TestCases and Assets to test django application fast. TDD is supported + +- [Mission](#mission) +- [Open Source Project](#open-source-project) +- [Features](#features) +- [Requirements](#requirements) +- [Development Status](#development-status) +- [Install](#install) +- [Quickstart](#quickstart) +- [Contributing](#contributing) + +## Mission + +The mission of the **Django DRY Tests** to design and develop open source python package to test **django** +application +- fast +- with minimal code duplication +- with the ability to use **TDD** easily +- simple and comfortable + +## Open Source Project + +This is the open source project with [MIT license](LICENSE). +Be free to use, fork, clone and contribute. + +## Features + +- Special **Request** and **Response** classes to simple set test data +- Special **SimpleTestCase** class with: + - `assertTrueResponse(self, current_response, true_response)` - Main assert to compare real response with expected + - `assertResponsesAreTrue(self, response_pairs)` - Compare many responses + - Other more simple asserts (`assertStatusCode`, `assertRedirectUrl`, `assertValueInContext`, + `assertContextValues`, `assertContentValues`) +- Special **TestCase** class. Similar with **SimpleTestCase** but with testing database (**Not ready yet**) + +## Requirements + +- `Django==4` (Lower versions haven't been tested) + +## Development Status + +- **django-dry-tests** +- **v0.1.0** +- **3 - Alpha** + +Package available on [PyPi](https://pypi.org/project/django-dry-tests/) + +## Install + +### with pip + +```commandline +pip install django-dry-tests +``` + +### from release page + +Download source code from [GitHub Releases page](https://github.com/quillcraftsman/django-dry-tests/releases) + +### clone from GitHub + +```commandline +git clone https://github.com/quillcraftsman/django-dry-tests.git +make install +``` + +## Quickstart + +For example, you need to test some view like this: + +```python +def index_view(request): + if request.method == 'GET': + context = { + 'title': 'Title' + } + return render(request, 'demo/index.html', context) + + name = request.POST.get('name', None) + if name is not None: + Simple.objects.create(name=name) + return HttpResponseRedirect('/') +``` + +And you want to check: +- GET response status code +- GET response context data +- GET response some html data +- POST response status code +- POST response redirect url +- POST response save object to database (**Not implemented yet**) + +Let`s see the tests code: +```python +from dry_tests import ( + Request, + TrueResponse as Response, + SimpleTestCase, + POST, +) + + +class ViewTestCase(SimpleTestCase): + + def test_main(self): + data = [ + # Multy parameters GET + { + 'request': Request(url='/'), + 'response': Response( + status_code=200, + in_context='title', + context_values={'title': 'Title'}, + content_values=['Title'], + ), + }, + # Multy parameters POST + { + 'request': Request(url='/', method=POST), + 'response': Response( + status_code=302, + redirect_url='/', + ), + }, + ] + for item in data: + request = item['request'] + true_response = item['response'] + current_response = request.get_url_response(self.client) + self.assertTrueResponse(current_response, true_response) +``` + +That's all this simple test cover all your test tasks with (**assertTrueResponse**) + +## Contributing + +You are welcome! To easy start please check: +- [Developer Guidelines](CONTRIBUTING.md) +- [Developer Documentation](DEVELOPER_DOCUMENTATION.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Security Policy](SECURITY.md) + diff --git a/setup.py b/setup.py index 0be1572..4948f66 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def read(filename): setup( name=PACKAGE_PYPI_NAME, - version="0.0.1", + version="0.1.0", packages=[PACKAGE_NAME], include_package_data=True, license="MIT", @@ -41,7 +41,7 @@ def read(filename): # ], python_requires=">=3", classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 3 - Alpha', "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3",