Go to Python: Unit testing
Go makes heavy use of interfaces to mock dependencies or for testing. Using Python the same way results in more verbose code than what is possible. So, I’m going to show you how to use unittest along with duck-typing/mocking for tests in Python.
For example, imagine you want to use HTTP to fetch a resource:
1# main.py
2import json
3import urllib.request
4import dataclasses
5
6@dataclasses.dataclass
7class Object:
8 id: str
9 name: str
10
11class HTTPClient:
12 def __init__(self, domain):
13 self.domain = domain
14
15 def get(self, object) -> Object:
16 url = f"https://{self.domain}/objects/{object}"
17 with urllib.request.urlopen(url) as r:
18 data = r.read()
19 charset = r.info().get_content_charset() or 'utf-8'
20 j = json.loads(data.decode(charset))
21 return Object(id=j['id'], name=j['name'])
22
23
24def get_message(client: HTTPClient, oid: str) -> str:
25 name = client.get(oid).name
26 return f"You have a short name: {name}" if len(name) < 5 else f"You have a long name: {name}"
27
28
29def main():
30 domain, oid = "api.restful-api.dev", "7"
31 client = HTTPClient(domain)
32 print(get_message(client, oid))
33
34if __name__ == '__main__':
35 main()
To write a mock test for get_message
, in Go, you’d define an interface which mocks the HTTPClient and write add a test which translates to something like this in Python:
1# main.py
2import json
3+import typing
4…
5
6@dataclasses.dataclass
7class Object:
8 id: str
9 name: str
10
11
12# Defininig a Client interface
13+class Client(typing.Protocol):
14+ def get(self, object) -> Object: ...
15
16
17# Mark that HTTPClient conforms to the `Client` protocol
18+class HTTPClient(Client):
19 ...
20…
21
22# Replacing type parameters
23-def get_message(client: HTTPClient, oid: str) -> str:
24+def get_message(client: Client, oid: str) -> str:
25 name = client.get(oid).name
26….
And now write the tests:
1# test_main.py
2import unittest
3from main import Object, Client
4
5# Also conforms to the `Client` protocol
6class StubClient(Client):
7 def __init__(self, name):
8 self.name = name
9
10 def get(self, object: str) -> Object:
11 return Object(id=object, name=self.name)
12
13class TestMain(unittest.TestCase):
14 def test_get_message_apple(self):
15 from main import get_message
16
17 # Use StubClient in get_message
18 message = get_message(StubClient("Apple iPhone 16 Pro"), "123")
19 self.assertEqual(message, "It's an Apple device")
20
21 def test_get_message_nonapple(self):
22 from main import get_message
23
24 message = get_message(StubClient("Samsung Galaxy Fold 4"), "123")
25 self.assertEqual(message, "It's not an Apple device")
26
27if __name__ == '__main__':
28 unittest.main()
But you can make use of Python’s duck-typing instead to simplify things. Python allows you to replace any object/class’s methods dynamically. So, let’s use this to override the HTTPClient
object’s get
method.
1# test_main.py
2import unittest
3-from main import Object, Client
4+from main import Object, HTTPClient
5
6...
7
8+def get_mockclient(name):
9+ client = HTTPClient("")
10+ def mockget(object) -> Object:
11+ return Object(id=object, name=name)
12+ client.get = mockget
13+ return client
14
15-class StubClient(Client):
16- def __init__(self, name):
17- self.name = name
18
19- def get(self, object: str) -> Object:
20- return Object(id=object, name=self.name)
21
22class TestMain(unittest.TestCase):
23 def test_get_message_apple(self):
24 from main import get_message
25- message = get_message(StubClient("Apple iPhone 16 Pro"), "123")
26+ client = get_mockclient("Apple iPhone 16 Pro")
27+ message = get_message(client, "123")
28 self.assertEqual(message, "It's an Apple device")
29
30 def test_get_message_nonapple(self):
31 from main import get_message
32- message = get_message(StubClient("Samsung Galaxy Fold 4"), "123")
33+ client = get_mockclient("Samsung Galaxy Fold 4")
34+ message = get_message(client, "123")
35 self.assertEqual(message, "It's not an Apple device")
36
37...
Infact, Python’s unittest
has a mock package which has utilities to do the exact same thing.
1from unittest.mock import Mock
2
3...
4
5-def get_mockclient(name):
6- client = HTTPClient("")
7- def mockget(object) -> Object:
8- return Object(id=object, name=name)
9- client.get = mockget
10- return client
11
12
13
14class TestMain(unittest.TestCase):
15 def test_get_message_apple(self):
16 from main import get_message, HTTPClient, Object
17- client = get_mockclient("Apple iPhone 16 Pro")
18- message = get_message(client, "123")
19+ m = Mock(spec=HTTPClient)
20+ m.get.return_value = Object(id="123", name="Apple iPhone 16 Pro")
21
22 message = get_message(m, "123")
23 self.assertEqual(message, "It's an Apple device")
24
25 def test_get_message_nonapple(self):
26 from main import get_message, HTTPClient, Object
27
28- client = get_mockclient("Samsung Galaxy Fold 4")
29- message = get_message(client, "123")
30+ m = Mock(spec=HTTPClient)
31+ m.get.return_value = Object(id="123", name="Samsung Galaxy Fold 4")
32
33 message = get_message(m, "123")
34 self.assertEqual(message, "It's not an Apple device")
35
36...
You can see the full code here