Skip to content

Commit

Permalink
Add liveserver and liveclient fixtures (closes #24)
Browse files Browse the repository at this point in the history
  • Loading branch information
idlesign committed May 19, 2023
1 parent 0963841 commit 31acfa2
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ pytest-djangoapp changelog
==========================


Unreleased
----------
+ Add 'liveserver' and 'liveclient' fixtures (closes #24).


v1.1.0 [2023-03-18]
-------------------
+ Added 'command_makemigrations' fixture.
Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This exposes some useful tools for Django applications developers to facilitate
* Migrations
* Messages
* DB queries audit
* Live server & client UI testing
* etc.

Suitable for testing apps for Django 1.8+.
Expand Down
1 change: 1 addition & 0 deletions docs/source/fixtures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Fixtures
fixtures_messages
fixtures_migrations
fixtures_utils
fixtures_live
7 changes: 7 additions & 0 deletions docs/source/fixtures_live.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Live server
===========

These instruments allows you to lunch a live servers and live clients, e.g. for UI testing purposes.

.. automodule:: pytest_djangoapp.fixtures.live
:members:
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This exposes some useful tools for Django applications developers to facilitate
* Migrations
* Messages
* DB queries audit
* Live server & client UI testing
* etc.

Suitable for testing apps for Django 1.8+.
Expand Down
1 change: 1 addition & 0 deletions pytest_djangoapp/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .commands import command_run, command_makemigrations
from .db import db_queries
from .live import liveserver, liveclient
from .mail import mail_outbox
from .messages import messages
from .migrations import check_migrations
Expand Down
140 changes: 140 additions & 0 deletions pytest_djangoapp/fixtures/live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from typing import Type, TypeVar, Dict

import pytest
from django.contrib.staticfiles.testing import StaticLiveServerTestCase

TypeLiveClient = TypeVar('TypeLiveClient', bound='LiveClient')


@pytest.fixture()
def liveserver() -> Type['LiveServer']:
"""Runs a live server. Available as a context manager.
Example::
def test_live(liveserver):
with liveserver() as server:
print(f'Live server is available at: {server.url}')
"""
return LiveServer


@pytest.fixture()
def liveclient():
"""Runs a live client. Available as a context manager.
Example::
def test_live(liveserver, liveclient):
with liveserver() as server:
# Let's run Firefox using Selenium.
with liveclient('selenium', browser='firefox') as client:
selenium = client.handle # Selenium driver is available in .handle
# Let's open server's root URL and check heading 1 on that page
selenium.get(server.url)
assert selenium.find_element('tag name', 'h1').text == 'Not Found'
"""

def get_client(typename: str, *, browser: str) -> TypeLiveClient:
return LiveClient.spawn(alias=typename, browser=browser)

return get_client


class LiveServer:

_cls = StaticLiveServerTestCase

def __init__(self, *, host: str = None, port: int = None):

cls = self._cls

if host:
cls.host = host

if port is not None:
cls.port = port

@property
def url(self) -> str:
"""URL to access this live server."""
return self._cls.live_server_url

def __enter__(self):
self._cls._start_server_thread()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self._cls._terminate_thread()


class LiveClient:
"""Base class for live clients."""

alias: str = ''

_registry: Dict[str, TypeLiveClient] = {}

def __init_subclass__(cls, **kwargs):
super().__init_subclass__()

alias = cls.alias

if alias:
cls._registry[alias] = cls

def __init__(self, *, browser: str):
self._browser = browser
self.handle = None

def __enter__(self) -> TypeLiveClient:
self.handle = self._handle_init()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self._handle_destroy()

@classmethod
def spawn(cls, alias: str, **kwargs) -> TypeLiveClient:
client_cls = LiveClient._registry[alias]
return client_cls(**kwargs)

def _handle_init(self): # pragma: nocover
raise NotImplementedError

def _handle_destroy(self):
pass


class SeleniumLiveClient(LiveClient):
"""This live client wraps Selenium.
https://selenium-python.readthedocs.io/
"""
alias: str = 'selenium'

def _handle_init(self):
from django.test.selenium import SeleniumTestCaseBase

cls = SeleniumTestCaseBase
browser = self._browser
driver_cls = cls.import_webdriver(browser)

return driver_cls(options=cls.import_options(browser)())

def _handle_destroy(self):

handle = self.handle

if handle:
handle.quit()

super()._handle_destroy()
20 changes: 20 additions & 0 deletions pytest_djangoapp/tests/test_fixtures_live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from unittest.mock import MagicMock


def test_liveserver(liveserver, liveclient):

with liveserver(host='localhost', port=33445) as server:
assert server.url == 'http://localhost:33445'


def test_selenium(liveserver, liveclient, monkeypatch):

monkeypatch.setattr('django.test.selenium.SeleniumTestCaseBase', MagicMock())

with liveserver() as server:

with liveclient('selenium', browser='firefox') as client:
selenium = client.handle

selenium.get(server.url)
assert 'MagicMock' in f"{selenium.find_element('tag name', 'h1').text}"

0 comments on commit 31acfa2

Please sign in to comment.