Skip to content

Instantly share code, notes, and snippets.

@Matt3o12
Last active August 29, 2015 14:04
Show Gist options
  • Save Matt3o12/03d0b33b4f406871e3e4 to your computer and use it in GitHub Desktop.
Save Matt3o12/03d0b33b4f406871e3e4 to your computer and use it in GitHub Desktop.
This test case creates a remote PostreSQL database provided by [postgression.com](http://postgression.com) and saves the credentials in order to limit the databases needed for testing.

What does this TestCase

This test case creates a remote PostreSQL database provided by postgression.com. postgression.com provides you with a PostgreSQL database, so you don't need to set up a PostgreSQL server on your local machine.
Unfortunately, PostgreSQL only provides you with 100 Database per hour, which can be reached very quickly when running lots of tests at the same time. This is where this DatabaseTestCase comes into place. It stores information about the last database used on your machine and reuses it so you won't reach the limit of 100 database.

How do I install/implement this?

Just copy the TestCase class somewhere in your tests folder. Then import the class. Let's say you have a file called setupTests.py in the test folder, the you would have to write:

from .setupTests import DatabaseTestCase

class MyTests(DatabaseTestCase):
    def myTest(self):
        host = self.postgreSQL_host
        # your test goes here.

Where are the login credentials for the Database?

The TestCase has the following properties for that: postgreSQL_host, postgreSQL_port, postgreSQL_dbname, postgreSQL_user, postgreSQL_password.

What if my class needs its own setUp method?

Since DatabaseTestCase overrides setUp(self), you will need to call it from your class. Simply add super().setUp() to the top of your custom setUp(self) method.

FAQ

Will the database expire if my tests take longer than ...?

Even if your tests take 3 hours, the Database will never expire. After each test, TestDatabase checks whether the database is older than 10 minutes and if so, it will create a new Database. So, if every tests take less than 20 minutes, you're good to go. If a test takes more than 20 minutes, you might want to split the test into different tests, anyway. It will make debugging easier, I promise :)

Can the Database expire during a test?

A database provided by postgression.com will expire after 30 minutes. By default DatabaseTestCase will create a new Database if the last database is older than 10 minutes. So, as long as non of your tests take longer than 20 minutes, you're good to go. If one of your tests takes longer than 20 minutes the database may expire. You can change the maximal age of a database by override the max_database_age property (simply add max_database_age = 600 to the top of your TestCase). Keep in mind that the age should be in seconds, not minutes.

Can I decrease/increase the maximal age of a database

If you want your database to last (less) longer, you will need to update the max_database_age property. If you want your database to expire after one minute, you will need to have your class set up like this:

class MyTests(DatabaseTestCase):
    max_database_age = 60 # in seconds

Where are the information of the database stored?

All information created by DatabaseTestCase are saved under .database.json by default. If you need to save them somewhere else, update the property database_file with the new path, as you would set max_database_age.

My coworkers and I reach the API limit even though we use this script!

Remember, the limit is set at 100 database per hour per IP. So, if one of your coworkers (who is on the same network) uses 95 Database, you only will only have 5 more to use. You can limit the databases you use by setting setting the max_database_age to 25 minutes and you will only use 2 to 3 database per hour (you can find more information on how to do that in the FAQs). If all coworker use the same script, and you need to share it, you could set the database_file to point to a shared network on each computer.

Something terrible happened and I need a new Database!

Sometime, you will mess up your Database due to a bug in your code. Your database may become unreliable and may not work as expected anymore. In this case you will need a new Database. You can do that by removing the .database.json in your working directory. It will make the script request a new one from the

I want a new Database for every test!

Well, even though you may want a complete new Database every time a test gets executed, it isn't that easy. Remember, you one have 100 databases per hour, so you can't get a new database for every test because If you have 60 tests (which isn't much), you could only run them all once per hour.
A good approach at solving this problem is using a transaction for every unit test and rollback after the test. If you can't do that, you could also override tearDown() (don't forget to call super!) and truncate/drop all database but this is 2 times slower than a rollback.
If you really don't create if you reach the api limit, you can set the max_database_age to 0, which causes the Database to load from the server every time a new test is executed (so, quite a few times).

import os
import unittest, json
import requests
import time
class DatabaseTestCase(unittest.TestCase):
max_database_age = 10 * 60 # 10min
database_file = ".database.json"
postgreSQL_dbname = None
postgreSQL_user = None
postgreSQL_password = None
postgreSQL_port = None
postgreSQL_host = None
def _loadDatabaseConfig(self, file):
"""
Loads database config from file, sets the position to 0.
:param file: the file to load the config from.
:return: dict or None
"""
if file.seek(0, os.SEEK_END) == 0:
return None
else:
file.seek(0)
config = json.load(file)
file.seek(0)
return config
def _saveConfigToFile(self, config, file):
"""
Saves the config to file.
:param config: dict
:param file: file-like object
:return: None
"""
if not isinstance(config, dict):
raise TypeError("Config needs to be a dict, got {} instead.".format(config))
if file.tell() != 0:
file.seek(0)
file.truncate()
json.dump(config, file)
file.seek(0)
def _loadDatabaseConfigFromServer(self):
"""
Loads the file from server
:return: dict containing the config.
"""
headers = {"Accept": "application/json"}
response = requests.get("http://api.postgression.com", headers=headers)
if response.status_code != 200:
msg = "Couldn't load database. API returned with invalid status code: {}"
raise AssertionError(msg.format(response.status_code))
config = {
"timestamp": time.time(),
"postgreSQL": response.json()
}
return config
def setUp(self):
"""
Loads the tests from the server, if appropriate, saves the config and sets
variables
:return:
"""
with open(self.database_file, "a+t") as file:
file.seek(0)
config = self._loadDatabaseConfig(file)
recreateConfig = False
if not config:
recreateConfig = True
elif (time.time() - self.max_database_age) > config["timestamp"]:
recreateConfig = True
if recreateConfig:
config = self._loadDatabaseConfigFromServer()
self._saveConfigToFile(config, file)
self.postgreSQL_dbname = config["postgreSQL"]["dbname"]
self.postgreSQL_host = config["postgreSQL"]["host"]
self.postgreSQL_password = config["postgreSQL"]["password"]
self.postgreSQL_port = config["postgreSQL"]["port"]
self.postgreSQL_user = config["postgreSQL"]["username"]
def tearDown(self):
self.postgreSQL_dbname = None
self.postgreSQL_host = None
self.postgreSQL_password = None
self.postgreSQL_port = None
self.postgreSQL_user = None
import json
import re
import unittest
import io
from unittest.mock import MagicMock, patch
import requests
from setupTests import DatabaseTestCase
@patch("os.path.isfile", new=lambda *s: True)
class TestDatabaseTestCase(unittest.TestCase):
def testLoadDatabaseConfig_emptyFile(self):
file = io.StringIO()
result = DatabaseTestCase()._loadDatabaseConfig(file)
self.assertIsNone(result)
self.assertEqual(0, file.tell())
def testLoadDatabaseConfig(self):
file = io.StringIO('{"foo": "bar"}')
result = DatabaseTestCase()._loadDatabaseConfig(file)
self.assertEqual({"foo": "bar"}, result)
self.assertEqual(0, file.tell())
@patch("requests.get")
def testLoadDatabaseFromServer_invalidStatus(self, getMock):
responseMock = MagicMock(requests.Response)
responseMock.configure_mock(
status_code = 404
)
getMock.return_value = responseMock
msg = "Couldn't load database. API returned with invalid status code: 404"
with self.assertRaisesRegex(AssertionError, re.escape(msg)):
DatabaseTestCase()._loadDatabaseConfigFromServer()
def testSaveConfigToFile(self):
file = io.StringIO()
config = {"foo": "bar"}
DatabaseTestCase()._saveConfigToFile(config, file)
self.assertEqual('{"foo": "bar"}', file.getvalue())
self.assertEqual(0, file.tell())
def testSaveConfigToFile_invalidArg(self):
with self.assertRaises(TypeError):
DatabaseTestCase()._saveConfigToFile(object, None)
@patch("requests.get")
@patch("time.time", new=lambda *a: 100.001)
def testLoadDatabaseConfigFromServer(self, getMock):
jsonResponse = {
'username': 'username',
'port': 5432,
'dbname': 'database',
'password': 'secret',
'host': 'host.org'
}
responseMock = MagicMock(requests.Response)
responseMock.json.return_value = jsonResponse
responseMock.configure_mock(
status_code = 200
)
getMock.return_value = responseMock
config = DatabaseTestCase()._loadDatabaseConfigFromServer()
expected = {
"timestamp": 100.001,
"postgreSQL": {
'username': 'username',
'port': 5432,
'dbname': 'database',
'password': 'secret',
'host': 'host.org'
}
}
self.assertEqual(expected, config)
@patch("builtins.open")
@patch.object(DatabaseTestCase, "_loadDatabaseConfigFromServer")
def testSetUp_noConfigFile(self, loadFromServerMock, openMock):
file = io.StringIO()
openMock.return_value = file
case = DatabaseTestCase()
with patch.object(case, "_loadDatabaseConfigFromServer") as loadFromServerMock:
with patch.object(case, "_saveConfigToFile") as saveConfigToFileMock:
cfg = {
"postgreSQL": {
'username': 'postgreUser',
'port': 1243,
'dbname': 'my_database',
'password': 'secret',
'host': 'host.org'
}
}
loadFromServerMock.return_value = cfg
case.setUp()
loadFromServerMock.assert_called_once_with()
saveConfigToFileMock.assert_called_once_with(cfg, file)
self.assertEqual("postgreUser", case.postgreSQL_user)
self.assertEqual(1243, case.postgreSQL_port)
self.assertEqual("my_database", case.postgreSQL_dbname)
self.assertEqual("secret", case.postgreSQL_password)
self.assertEqual("host.org", case.postgreSQL_host)
@patch("builtins.open")
@patch("time.time")
def testSetUpClass_databaseExpired(self, timeMock, openMock):
self.assertEqual(600, DatabaseTestCase.max_database_age)
config = {
"timestamp": 1000,
}
timeMock.return_value = 1601
file = io.StringIO(json.dumps(config))
openMock.return_value = file
case = DatabaseTestCase()
with patch.object(case, "_loadDatabaseConfigFromServer") as loadFromServerMock:
with patch.object(case, "_saveConfigToFile") as saveConfigToFileMock:
cfg = {
"postgreSQL": {
'username': 'postgreUser',
'port': 1243,
'dbname': 'my_database',
'password': 'secret',
'host': 'host.org'
}
}
loadFromServerMock.return_value = cfg
case.setUp()
loadFromServerMock.assert_called_once_with()
saveConfigToFileMock.assert_called_once_with(cfg, file)
self.assertEqual("postgreUser", case.postgreSQL_user)
self.assertEqual(1243, case.postgreSQL_port)
self.assertEqual("my_database", case.postgreSQL_dbname)
self.assertEqual("secret", case.postgreSQL_password)
self.assertEqual("host.org", case.postgreSQL_host)
@patch("builtins.open")
@patch("time.time")
def testSetUpClass(self, timeMock, openMock):
self.assertEqual(600, DatabaseTestCase.max_database_age)
config = {
"timestamp": 1000,
"postgreSQL": {
'username': 'postgreUser',
'port': 1243,
'dbname': 'my_database',
'password': 'secret',
'host': 'host.org'
}
}
timeMock.return_value = 1500
file = io.StringIO(json.dumps(config))
openMock.return_value = file
case = DatabaseTestCase()
with patch.object(case, "_loadDatabaseConfigFromServer") as loadFromServerMock:
case.setUp()
self.assertFalse(loadFromServerMock.called)
self.assertEqual("postgreUser", case.postgreSQL_user)
self.assertEqual(1243, case.postgreSQL_port)
self.assertEqual("my_database", case.postgreSQL_dbname)
self.assertEqual("secret", case.postgreSQL_password)
self.assertEqual("host.org", case.postgreSQL_host)
def testSaveConfigToFile_nonEmptyFile(self):
file = io.StringIO("This file is not empty")
config = {
"foo": "bar"
}
DatabaseTestCase()._saveConfigToFile(config, file)
self.assertEqual('{"foo": "bar"}', file.getvalue())
def testSaveConfigToFile_nonEmptyFile_middleOfFile(self):
file = io.StringIO("This file is not empty")
file.seek(4)
config = {
"foo": "bar"
}
DatabaseTestCase()._saveConfigToFile(config, file)
self.assertEqual('{"foo": "bar"}', file.getvalue())
def testSaveConfigToFile_nonEmptyFile_endOfFile(self):
file = io.StringIO("This file is not empty")
file.seek(0, io.SEEK_END)
config = {
"foo": "bar"
}
DatabaseTestCase()._saveConfigToFile(config, file)
self.assertEqual('{"foo": "bar"}', file.getvalue())
def testTearDown(self):
case = DatabaseTestCase()
case.postgreSQL_dbname = "Some value."
case.postgreSQL_host = "Some value."
case.postgreSQL_password = "Some value."
case.postgreSQL_port = "Some value."
case.postgreSQL_user = "Some value."
case.tearDown()
self.assertIsNone(case.postgreSQL_password)
self.assertIsNone(case.postgreSQL_port)
self.assertIsNone(case.postgreSQL_dbname)
self.assertIsNone(case.postgreSQL_user)
self.assertIsNone(case.postgreSQL_host)
# class MyTestCase(DatabaseTestCase):
# def testSomething(self):
# self.assertIsNotNone(self.postgreSQL_password)
# self.assertIsNotNone(self.postgreSQL_port)
# self.assertIsNotNone(self.postgreSQL_dbname)
# self.assertIsNotNone(self.postgreSQL_user)
# self.assertIsNotNone(self.postgreSQL_host)
if __name__ == "__main__":
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment