Skip to content

Instantly share code, notes, and snippets.

@lassebenni
Last active April 30, 2020 11:12
Show Gist options
  • Save lassebenni/7662da070e882988ec3bc99a25bb8e4d to your computer and use it in GitHub Desktop.
Save lassebenni/7662da070e882988ec3bc99a25bb8e4d to your computer and use it in GitHub Desktop.
Python language

Memory allocation in Python

Python variables are fundamentally different than variables in C or C++. In fact, Python doesn’t even have variables. Python has names, not variables.

PyTest

Testing your code is essential. It provides a base of reference when you amend/add code. Example: You write function A. You write a passing unit test for function A. Superb. Now your code is covered and you have prepared for all possible failing cases (sarcasm applied). Skip a week. Now we write a new function, which applies function A. You don't exactly remember what function A did, but you know what you expect it to return. You run your tests after building the code. Suddenly your unit test breaks. Since you already wrote it with multiple edge-cases, you should exactly know why it failed, perhaps you passed a wrong parameter. You fix the error, and your unit test passes again. Past-you just saved present-you a lot of time.  The more complicated and longer your functions are the more time you save. No unit-tests means: scanning over your code, one line at a time. Might not seem bad when you wrote the function that same morning. Becomes a P.I.T.A when it's code you wrote a year ago. Even worse when it's code someone else wrote a year ago. Anyhow, at least that's how I convinced myself that unit-tests are important.

Py.Test

Python offers the comprehensive Py.Test module for unit-testing. Their excellent documentation provides many hours worth of walking through examples and background information. But in essence, the way of testing is simple:

content of test_sample.py

def inc(x):

return x + 1

def test_answer():

assert inc(3) == 5

First we define our test function. In this case it will test our inc function, which is in the same file (usually you don't define tests in the same file as the functions you wrote). It just asserts ("checks if true") that inc(3) is 5 (which it shouldn't be).

$ pytest

=========================== test session starts ============================

test_sample.py F [100%]

================================= FAILURES =================================

_______________________________ test_answer ________________________________

def test_answer():

assert inc(3) == 5

E assert 4 == 5

E + where 4 = inc(3)

test_sample.py:6: AssertionError

========================= 1 failed in 0.12 seconds =========================

We ran pytest from the commandline which prompts it to start looking for files that contain the name "test_" or "_test" and run testfunctions in there. In our example it throws an error because we got the answer 4 and that is not 5. Test done.

In Pytest you use simple assert statements, like assert 5 == len(array). That alone, to me, feels cleaner and more clear. I am not an experienced unit-test writer (yet), so it keeps it simple. As with most frameworks however, there are some gotcha's to PyTest. I just wrote some stuff down while I was rifling through the documentation:

Fixtures are cool. They provide a dependency-injection of objects into your test functions:

content of ./test_smtpsimple.py

import pytest

@pytest.fixture

def smtp():

import smtplib

return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

def test_ehlo(smtp):

response, msg = smtp.ehlo()

assert response == 250

assert 0 # for demo purposes

Above, we define a fixture with pytest.fixture annotation (the "@" symbol stands for annotation, which is code that runs/is coupled with the code underneath it). Now we can "inject" the fixture called "smtp" into our test "test_ehlo". We didn't have to explicitely create the "smtp"-object inside the "test_ehlo" function, we can just immediately start using it.

Some other stuff Fixtures can do:

Parametrization (adding parameters to them).

Re-using fixtures throughout the Session/Module/Class.

Setup & Teardown fixures (create and destroy to prevent errors).

Autofixtures (fixtures are run before each test). Especially useful for chaining fixtures.

All the above and more can be found in the Fixtures Documentation.

Monkeypatching. This technique is used to temporarily 'patch' (change) a value for the duration of a function. Sounds a bit vague, here's an example I stole:

content of test_module.py

import os.path

def getssh(): # pseudo application code

return os.path.join(os.path.expanduser("~admin"), '.ssh')

def test_mytest(monkeypatch):

def mockreturn(path):

return '/abc'

monkeypatch.setattr(os.path, 'expanduser', mockreturn)

x = getssh()

assert x == '/abc/.ssh'

"Here our test function monkeypatches os.path.expanduser and then calls into a function that calls it. After the test function finishes the os.path.expanduser modification will be undone."

So we basically want to change the function call of expanduser to always return "/abc" whenever we call it. For this we can use monkeypatch.

monkeypatch.setattr(os.path, 'expanduser', mockreturn)

We inject the monkeypatch dependency in the function (notice the monkeypatch parameter that test_mytest accepts). Then we define a function that will replace expanduser functionality. Since functions are objects and furthermore can be attributes on other objects, we can overwrite the expanduser "function attribute" on the os.path object with our mockreturn function. Now, when we call getssh(), os.path.expanduser is called and returns '/abc' because of our mockreturn function. When the function has completed, the os.path.expand attribute returns to normal.

Monkeypatching is used for "functionality which depends on global settings or which invokes code which cannot be easily tested such as network acces". It can be used in various ways, usually by changing functionality that you want to prevent.

Parametrization

Another big one is parameterizing (weird word). Here we define our input data and expected result as parameters to our test_functions:

testdata = [

(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),

(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),

]

@pytest.mark.parametrize("a,b,expected", testdata)

def test_timedistance_v0(a, b, expected):

diff = a - b

assert diff == expected

You could implement the above functionality yourself with some nested lists, but Py.Test saves us the hassle with predefined options "a" for the first object, "b" for the second, and "expected" for the last. This can be passed around to other test_functions. We can do much more complex tricks (testscenarios) than this, but I will leave this to you to read.

This should give you a very basic insight in the PyTest module. Enough to create your own tests and play around with fixtures before diving deeper into the documentation. Happy testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment