SlideShare a Scribd company logo
Mock Hell
PyConDE and PyData Berlin, 2019
edwin.jung@outlook.com
https://www.linkedin.com/in/junged/
twitter: @ejjcatx
•Backend Engineer
@ Quid
•Python => 1st
dynamic language
edwin.jung@outlook.com
https://www.linkedin.com/in/junged/
twitter: @ejjcatx
Mock Hell PyCon DE and PyData Berlin 2019
Mock Hell PyCon DE and PyData Berlin 2019
Perfect isolation,
100% coverage
Tests that test
nothing
Reverse-
engineering
mocks with the
debugger
Mocks that
prevent
refactoring
Lust
Gluttony
Greed
Fraud
Treachery
Heresy
Confusing patch
targets
Brittle assertions
Complex Setup
Deep/recursive
mocks; mocks-in-
mocks
Too many mocks
or patches
Limbo
“A Mock Tutorial”
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
# test_core.py
from core import total_value
def test_total_value():
assert 300. == total_value(“karl@python.de”, 92)
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
# test_core.py
from unittest.mock import patch, call
from core import total_value
def test_total_value():
with patch('core.db_read') as mock_read:
assert 300. == total_value(“karl@python.de”, 92)
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
# test_core.py
from unittest.mock import patch, call
from core import total_value
def test_total_value():
with patch('core.db_read') as mock_read:
mock_read.return_value = [100, 200]
assert 300. == total_value(“karl@python.de”, 92)
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
# test_core.py
from unittest.mock import patch, call
from core import total_value
def test_total_value():
with patch(‘core.db_read’) as mock_read:
mock_read.return_value = [100, 200]
assert 300. == total_value(“karl@python.de”, 92)
assert [call(“karl@python.de", inv_no=92)] == mock_read.calls
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
db
read
total
value
total
value
db
read
test
total
value
test
db
read
total
value
test
db
read
mock
read
total
value
test
test
total
value
mock
read
Real World Example:
NewsCheck
News
Scraper
Internet S3
You have a large corpus of scraped
public news articles stored in S3, for
use in NLP experiments.
Bad Article
Market Movers
Data as of Jul 19
Friday’s Close:
Dow-68.77
27,154.20 -0.25%Nasdaq-60.754
2,976.61 -0.62%
S&P 500 | S&P 1500
Most Actives
Company Price Change % Change
MSFT Microsoft Corp 136.62 +0.20 +0.15%
BAC Bank of America Corp 29.40 -0.08-0.27%
…
MU Micron Technology Inc 45.52 +0.85
+1.90%
F Ford Motor Co 10.20 -0.06-0.58%
GE General Electric Co 10.04 -0.02-0.20%
T AT&T Inc 32.79 -0.30-0.91%
…
FCX Freeport-McMoRan Inc 11.49 +0.34
+3.05%
INTC Intel Corp 50.27 +0.33 +0.66%
PFE Pfizer Inc 42.77 -0.29-0.67%
>50% lines stock symbols
Write a script that scans your news archive
to check for stock quotes articles.
s3://my_bucket/news/2.txt
S3 Vocabulary
Key
Prefix
Bucket
S3
• To scan everything in a bucket:
• Get a paginated list of keys
• Download each object by key
{
"Name": “my_bucket”,
"Prefix": "news",
"MaxKeys": 1000,
"Contents": [
{
"Key": "news/",
"LastModified": "2019-07-21",
"Size": 0
},
{
"Key": "news/1.txt",
"LastModified": "2019-07-21",
"Size": 12695
},
...
{
"Key": "news/3.txt",
"LastModified": "2019-07-21",
"Size": 1587
}
],
...
}
├── newscheck
│   ├── __init__.py
│   ├── config.py
│   ├── stock.py
│   ├── core.py
│   ├── util.py
│   └── test_core.py
├── script.py
├── newscheck
│   ├── __init__.py
│   ├── config.py
│   ├── stock.py
│   ├── core.py
│   ├── util.py
│   └── test_core.py
├── script.py
# script.py
from newscheck.core import find_bad_articles
if __name__ == "__main__":
print(list(find_bad_articles(“my_bucket”)))
├── newscheck
│   ├── __init__.py
│   ├── config.py
│   ├── stock.py
│   ├── core.py
│   ├── util.py
│   └── test_core.py
├── script.py
# core.py
import boto3
import newscheck.config as config
import newscheck.stock as stock
from newscheck.util import *
def find_bad_articles(bucket_name):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS):
yield key
├── newscheck
│   ├── __init__.py
│   ├── config.py
│   ├── stock.py
│   ├── core.py
│   ├── util.py
│   └── test_core.py
├── script.py
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
def get_pages(client, bucket_name, prefix):
paginator = client.get_paginator('list_objects')
page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix)
return page_iterator
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
def keys_from(page):
for item in page['Contents']:
key = item['Key']
if key == f"{config.PREFIX}/": #ignore root
continue
yield key
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): —
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page): ---
def get_file(client, bucket_name, key):
buf = io.BytesIO()
client.download_fileobj(bucket_name, key, buf)
# Decode and drop empty lines
buf.seek(0)
lines = (line.decode().strip() for line in buf.readlines())
return list(filter(lambda x:x, lines))
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): —
def check_article(lines, threshold, stock_symbols):
limit = threshold * len(lines)
while lines and limit:
line = lines.pop()
first_word = line.split()[0]
if first_word in stock_symbols:
limit -= 1
return limit <= 0
# util.py
import newscheck.config as config
+---- 4 lines: def get_pages(client, bucket_name, prefix): ---
+---- 6 lines: def keys_from(page):
+---- 7 lines: def get_file(client, bucket_name, key): ---
+--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
├── newscheck
│   ├── __init__.py
│   ├── config.py
│   ├── stock.py
│   ├── core.py
│   ├── util.py
│   └── test_core.py
├── script.py
# test_core.py
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
# Test something here!
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
import boto3
import newscheck.config as config
import newscheck.stock as stock
from newscheck.util import *
def find_bad_articles(bucket_name):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS):
yield key
Changing Imports?
import boto3.client as s3client
import newscheck.config as config
import newscheck.stock as stock
from newscheck.util import *
def find_bad_articles(bucket_name):
client = s3client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS):
yield key
Changing Imports?
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
Breaks this
patch
import boto3
import newscheck.config as config
import newscheck.stock as stock
from newscheck.util import *
def find_bad_articles(bucket_name):
client = boto3.client(‘s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS):
yield key
Args to Kwargs?
import boto3
import newscheck.config as config
import newscheck.stock as stock
from newscheck.util import *
def find_bad_articles(bucket_name):
client = boto3.client(‘s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key=key)
if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS):
yield key
Args to Kwargs?
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
Breaks these
assertions
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
Missing an
assertion
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
What if you want to
support tarballs
instead of S3?
from unittest.mock import MagicMock, call, patch
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
@patch("newscheck.core.get_file")
@patch("newscheck.core.get_pages")
@patch("newscheck.core.boto3.client")
def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock):
page_stub = [{
"Contents": [
{"Key": 'news/'},
{"Key": 'news/good.txt'},
{"Key": 'news/bad.txt'}]
}]
get_pages_mock.return_value = page_stub
file_stubs = [
"lorem ipsum dolorumn foo baz bar bizn hello world".split('n'),
"MSFT 99.32n BAC 22.3n F 33.2n".split('n'),
]
get_file_mock.side_effect = file_stubs
bad_articles = list(find_bad_articles("my_bucket"))
self.assertEqual(["news/bad.txt"], bad_articles)
self.assertEqual([
call(client_mock.return_value, "my_bucket", "news/good.txt"),
call(client_mock.return_value, "my_bucket", "news/bad.txt")
],
get_file_mock.mock_calls
)
What if you
want different
checking logic?
What went wrong?
What went wrong?
Mocking is for:
•test isolation
•simulating dependencies
•avoiding side-effects
What went wrong?
Mocking is for:
•test isolation
•simulating dependencies
•avoiding side-effects
(Be careful about overmocking!)
What went wrong?
Mocking is for:
•test isolation
•simulating dependencies
•avoiding side-effects
(Be careful about overmocking!)
What went wrong?
Mock Hell PyCon DE and PyData Berlin 2019
Mock Hell PyCon DE and PyData Berlin 2019
Mock Objects is an
extension to Test
Driven Development
that supports good
Object-Oriented
design …
It[’s] less interesting as
a technique for isolating
tests … than is widely
thought.
What went wrong.
Mocking is supposed to be a technique for (1) Object
Oriented Design, in the context of (2) TDD, for
exploratory design and discovery.
Mocking is not just a tool for test isolation.
What went wrong.
Mocking is supposed to be a technique for (1) Object
Oriented Design, in the context of (2) TDD, for
exploratory design and discovery.
Mocking is not just a tool for test isolation.
“[Dependency Injection]
… is a virtue.”
Freeman and Pryce advocate what
many call “the London school of TDD,
…”
I’m not fully disagreeing …  but for
maintainability … tests using mocks
creates more trouble than it’s
worth.
“Mocks aren’t Stubs”
“Mocks aren’t Stubs”
A mock is a kind of test double. Other kinds of
test doubles include stubs, fakes, spies,
dummies.
“Mocks aren’t Stubs”
A mock is a kind of test double. Other kinds of
test doubles include stubs, fakes, spies,
dummies.
The difference between different “schools” of TDD,
is how you use test doubles.
from unittest.mock import patch, call
from my_module import total_value
def test_total_value():
with patch('my_module.db_read') as mock_read:
mock_read.return_value = [100, 200]
assert 300. == total_value(“karl@python.de”, 92)
assert [call(“karl@python.de”, inv_no=92)] == mock_read.calls
Option: Patch a Mock
from unittest.mock import patch, call
from my_module import total_value
def test_total_value():
with patch('my_module.db_read') as mock_read:
mock_read.return_value = [100, 200]
assert 300. == total_value(“karl@python.de”, 92)
assert [call(“karl@python.de”, inv_no=92)] == mock_read.calls
stub returns
canned
values
Option: Patch a Mock
from unittest.mock import patch, call
from my_module import total_value
def test_total_value():
with patch('my_module.db_read') as mock_read:
mock_read.return_value = [100, 200]
assert 300. == total_value(“karl@python.de”, 92)
assert [call(“karl@python.de”, inv_no=92)] == mock_read.calls
mock validates
interaction
stub returns
canned
values
Option: Patch a Mock
Option: Inject a Mock
# core.py
from db import db_read
def total_value(email, invoice_id, discount=0.0):
items = db_read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
# core.py
def total_value(read, email, invoice_id, discount=0.0):
items = read(email, inv_no=invoice_id)
return sum(items)*(1.0-discount)
Option: Inject a Mock
def test_total_value():
assert 300 == total_value(???, “karl@python.de”, 92)
Option: Inject a Mock
Option: Inject a Mock
def test_total_value():
mock_read = MagicMock()
mock_read.return_value = [100, 200]
assert 300 == total_value(???, “karl@python.de”, 92)
Option: Inject a Mock
def test_total_value():
mock_read = MagicMock()
mock_read.return_value = [100, 200]
assert 300 == total_value(mock_read, “karl@python.de”, 92)
assert [call(“karl@python.de”, inv_no=92)] == mock_read.calls
def test_total_value():
assert 300 == total_value(???, “karl@python.de”, 92)
Option: Inject a Fake
def test_total_value():
def fake_db_read(email, invoice_id):
with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj:
return json.load(fobj)
assert 300 == total_value(???, “karl@python.de”, 92)
Option: Inject a Fake
def test_total_value():
def fake_db_read(email, invoice_id):
with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj:
return json.load(fobj)
assert 300 == total_value(fake_db_read, “karl@python.de”, 92)
Option: Inject a Fake
def test_total_value():
def fake_db_read(email, invoice_id):
with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj:
return json.load(fobj)
assert 300 == total_value(fake_db_read, “karl@python.de”, 92)
def test_total_value():
mock_read = MagicMock()
mock_read.return_value = [100, 200]
assert 300. == total_value(mock_read, “karl@python.de”, 92)
assert [call(“karl@python.de”, inv_no=92)] == mock_read.calls
Mockist
Classical
db
read
total
value
read
total
value
test
read
total
value
fake
read
test
read
total
value
fake
read
total
value
db
read
app
total
value
db
read
app
Ports and Adapters
total
value
test
abs.
db_read
db
readapp
fake
Port
Adapter
Adapter
Hexagonal
total
value
test
abs.
db_read
db
readapp
fake
Hexagonal
total
value
test
abs.
db_read
db
readapp
fake
“Listening to the Tests”
def find_bad_articles(bucket_name):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS):
yield key
if __name__ == "__main__":
print(list(find_bad_articles(“my_bucket”)))
To decouple check_article
Inject a Predicate
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
if __name__ == "__main__":
print(list(find_bad_articles(“my_bucket”, predicate=???)))
Using a Closure
if __name__ == "__main__":
def is_stock_quotes(lines):
return check_article(lines,
config.THRESHOLD,
config.STOCK_SYMBOLS)
print(list(find_bad_articles(“my_bucket”, is_stock_quotes)))
“Dependency Injection is
just passing things in.”
“Dependency Injection is
just passing things in.”
What do you inject?
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
boto3?
def find_bad_articles(bucket_name, predicate, boto3):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
client?
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
client?
def find_bad_articles(bucket_name, predicate, client):
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
get_pages, get_file…?
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
But what about keys_from?
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
Remember: Dependency Injection and
Mocking are Object Oriented Design
Single Responsibility
Open-Closed
Liskov Substitution
Interface Segregration
Dependency Inversion
find
bad
keys
from
get
pages
script
get
file
pred-
icate
The Archive Source
keys
from
get
pages
script
get
file
find
bad
pred-
icate
find
bad
keys
from
get
pages
script
get
file
pred-
icate
find
bad
keys
from
get
pages
script
get
file
Archive
S3
Archive
pred-
icate
find
bad
keys
from
get
pages
script
get
file
Archive
S3
Archive
“Object”
“Role”
“Mock Roles,
Not Objects”pred-
icate
find
bad
keys
from
get
pages
script
get
file
Archive
S3
Archive
“Detail depends on Abstraction”
Dependency
Inversion
depends on
pred-
icate
Extract
def find_bad_articles(bucket_name, predicate):
client = boto3.client('s3', region_name=config.REGION_NAME)
for page in get_pages(client, bucket_name, config.PREFIX):
for key in keys_from(page):
lines = get_file(client, bucket_name, key)
if predicate(lines):
yield key
def find_bad_articles(archive, predicate):
for page in archive.get_pages():
for key in archive.keys_from(page):
lines = archive.get_file(key)
if predicate(lines):
yield key
Extract
Encapsulate
class S3Archive(object):
def __init__(self, bucket_name, prefix, region_name):
"""Ctor"""
self.client = boto3.client('s3', region_name=region_name)
self.bucket_name = bucket_name
self.prefix = prefix
+---- 7 lines: def get_file(self, key): --------------------------
+---- 4 lines: def get_pages(self): ------------------------------
+---- 6 lines: def keys_from(self, page): ------------------------
Inject in App
if __name__ == "__main__":
def is_stock_quotes(lines):
return check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS)
print(list(find_bad_articles(???, is_stock_quotes)))
Inject in App
if __name__ == "__main__":
def is_stock_quotes(lines):
return check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS)
archive = S3Archive(“my_bucket”, config.PREFIX, config.REGION_NAME)
print(list(find_bad_articles(archive, is_stock_quotes)))
def find_bad_articles(archive, predicate):
for page in archive.get_pages():
for key in archive.keys_from(page):
lines = archive.get_file(key)
if predicate(lines):
yield key
Who should be responsible for
Paging and Key Parsing?
def find_bad_articles(archive, predicate):
for key in archive.get_keys():
lines = archive.get_file(key)
if predicate(lines):
yield key
Delegate Responsibility to
Archive
class S3Archive(object):
def __init__(self, bucket_name, prefix, region_name):
"""Ctor"""
self.client = boto3.client('s3', region_name=region_name)
self.bucket_name = bucket_name
self.prefix = prefix
def get_keys(self):
"""Get an iterable of keys"""
for page in self.get_pages():
for key in self.keys_from(page):
yield key
+---- 7 lines: def get_file(self, key): --------------------------
+---- 4 lines: def get_pages(self): ------------------------------
+---- 6 lines: def keys_from(self, page): ------------------------
Delegate Responsibility to
Archive
def find_bad_articles(archive, predicate):
for key in archive.get_keys():
lines = archive.get_file(key)
if predicate(lines):
yield key
def find_bad_articles(archive, predicate):
for key in archive.get_keys():
lines = archive.get_file(key)
if predicate(lines):
yield key
KafkaArchive?
Kafka
Stream of Tuples
“news/2.txt”, “lorem ispum dolorumn …”
“news/1.txt”, “quot et foo bar det in n …”
“news/3.txt”, “foos pis o lot forum box n …”
“news/9.txt”, “MSFT 99.34 n F 33.34 …”
def find_bad_articles(archive, predicate):
for key in archive.get_keys():
lines = archive.get_file(key)
if predicate(lines):
yield key
def find_bad_articles(archive, predicate):
for key in archive.get_keys():
lines = archive.get_file(key)
if predicate(lines):
yield key
KafkaArchive leads
to Liskov Violation
class S3Archive(object):
def __init__(self, bucket_name, prefix, client):
"""Ctor"""
self.client = boto3.client('s3', region_name=region_name)
self.bucket_name = bucket_name
self.prefix = prefix
def items(self):
for key in self.get_keys():
lines = self.get_file(key)
yield key, lines
def get_keys(self):
"""Get an iterable of keys"""
for page in self.get_pages():
for key in self.keys_from(page):
yield key
+---- 7 lines: def get_file(self, key): --------------------------
+---- 4 lines: def get_pages(self): ------------------------------
+---- 6 lines: def keys_from(self, page): ------------------------
Hide Responsibility in Archive
def find_bad_articles(archive, predicate):
for key, lines in archive.items():
if predicate(lines):
yield key
Archive as a Tuple Stream
import unittest
from newscheck.core import find_bad_articles
class TestBadArticleFinder(unittest.TestCase):
def test_find_bad_articles(self):
data = {
"good.txt": ["lorem ipsum dolorum"],
"bad.txt": ["i am the bad article"]
}
def check(lines):
return lines[0] == "i am the bad article"
bad_articles = list(find_bad_articles(data.items(), check))
self.assertEqual(1, len(bad_articles))
self.assertEqual("bad.txt", bad_articles[0])
Test
Mock Hell PyCon DE and PyData Berlin 2019
edwin.jung@outlook.com
https://www.linkedin.com/in/junged/
twitter: @ejjcatx
Mocks aren’t Stubs (Fowler)
Clean Code (Bob Martin)
Growing Object-Oriented
Software, Guided by Tests
(Pryce, Freeman)
The Art of Unit Testing
(Osherove)
Ad

More Related Content

What's hot (19)

A Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP GeneratorsA Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP Generators
Mark Baker
 
A Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP GeneratorsA Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP Generators
Mark Baker
 
Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)
Leonardo Soto
 
Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)
Leonardo Soto
 
Patterns for slick database applications
Patterns for slick database applicationsPatterns for slick database applications
Patterns for slick database applications
Skills Matter
 
Looping the Loop with SPL Iterators
Looping the Loop with SPL IteratorsLooping the Loop with SPL Iterators
Looping the Loop with SPL Iterators
Mark Baker
 
関西PHP勉強会 php5.4つまみぐい
関西PHP勉強会 php5.4つまみぐい関西PHP勉強会 php5.4つまみぐい
関西PHP勉強会 php5.4つまみぐい
Hisateru Tanaka
 
Пишем для asyncio - Андрей Светлов, PyCon RU 2014
Пишем для asyncio - Андрей Светлов, PyCon RU 2014Пишем для asyncio - Андрей Светлов, PyCon RU 2014
Пишем для asyncio - Андрей Светлов, PyCon RU 2014
it-people
 
Asyncio
AsyncioAsyncio
Asyncio
Andrew Svetlov
 
Error Management: Future vs ZIO
Error Management: Future vs ZIOError Management: Future vs ZIO
Error Management: Future vs ZIO
John De Goes
 
Comparing 30 Elastic Search operations with Oracle SQL statements
Comparing 30 Elastic Search operations with Oracle SQL statementsComparing 30 Elastic Search operations with Oracle SQL statements
Comparing 30 Elastic Search operations with Oracle SQL statements
Lucas Jellema
 
Generated Power: PHP 5.5 Generators
Generated Power: PHP 5.5 GeneratorsGenerated Power: PHP 5.5 Generators
Generated Power: PHP 5.5 Generators
Mark Baker
 
C++Programming Language Tips Tricks Understandings
C++Programming Language Tips Tricks UnderstandingsC++Programming Language Tips Tricks Understandings
C++Programming Language Tips Tricks Understandings
gufranresearcher
 
Zero to SOLID
Zero to SOLIDZero to SOLID
Zero to SOLID
Vic Metcalfe
 
Extending grimoirelab
Extending grimoirelabExtending grimoirelab
Extending grimoirelab
Valerio Cosentino
 
Having Fun Programming!
Having Fun Programming!Having Fun Programming!
Having Fun Programming!
Aaron Patterson
 
Tools for Making Machine Learning more Reactive
Tools for Making Machine Learning more ReactiveTools for Making Machine Learning more Reactive
Tools for Making Machine Learning more Reactive
Jeff Smith
 
PofEAA and SQLAlchemy
PofEAA and SQLAlchemyPofEAA and SQLAlchemy
PofEAA and SQLAlchemy
Inada Naoki
 
Desenvolvendo em php cli
Desenvolvendo em php cliDesenvolvendo em php cli
Desenvolvendo em php cli
Thiago Paes
 
A Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP GeneratorsA Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP Generators
Mark Baker
 
A Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP GeneratorsA Functional Guide to Cat Herding with PHP Generators
A Functional Guide to Cat Herding with PHP Generators
Mark Baker
 
Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)
Leonardo Soto
 
Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)
Leonardo Soto
 
Patterns for slick database applications
Patterns for slick database applicationsPatterns for slick database applications
Patterns for slick database applications
Skills Matter
 
Looping the Loop with SPL Iterators
Looping the Loop with SPL IteratorsLooping the Loop with SPL Iterators
Looping the Loop with SPL Iterators
Mark Baker
 
関西PHP勉強会 php5.4つまみぐい
関西PHP勉強会 php5.4つまみぐい関西PHP勉強会 php5.4つまみぐい
関西PHP勉強会 php5.4つまみぐい
Hisateru Tanaka
 
Пишем для asyncio - Андрей Светлов, PyCon RU 2014
Пишем для asyncio - Андрей Светлов, PyCon RU 2014Пишем для asyncio - Андрей Светлов, PyCon RU 2014
Пишем для asyncio - Андрей Светлов, PyCon RU 2014
it-people
 
Error Management: Future vs ZIO
Error Management: Future vs ZIOError Management: Future vs ZIO
Error Management: Future vs ZIO
John De Goes
 
Comparing 30 Elastic Search operations with Oracle SQL statements
Comparing 30 Elastic Search operations with Oracle SQL statementsComparing 30 Elastic Search operations with Oracle SQL statements
Comparing 30 Elastic Search operations with Oracle SQL statements
Lucas Jellema
 
Generated Power: PHP 5.5 Generators
Generated Power: PHP 5.5 GeneratorsGenerated Power: PHP 5.5 Generators
Generated Power: PHP 5.5 Generators
Mark Baker
 
C++Programming Language Tips Tricks Understandings
C++Programming Language Tips Tricks UnderstandingsC++Programming Language Tips Tricks Understandings
C++Programming Language Tips Tricks Understandings
gufranresearcher
 
Tools for Making Machine Learning more Reactive
Tools for Making Machine Learning more ReactiveTools for Making Machine Learning more Reactive
Tools for Making Machine Learning more Reactive
Jeff Smith
 
PofEAA and SQLAlchemy
PofEAA and SQLAlchemyPofEAA and SQLAlchemy
PofEAA and SQLAlchemy
Inada Naoki
 
Desenvolvendo em php cli
Desenvolvendo em php cliDesenvolvendo em php cli
Desenvolvendo em php cli
Thiago Paes
 

Similar to Mock Hell PyCon DE and PyData Berlin 2019 (20)

Designing REST API automation tests in Kotlin
Designing REST API automation tests in KotlinDesigning REST API automation tests in Kotlin
Designing REST API automation tests in Kotlin
Dmitriy Sobko
 
Web2py Code Lab
Web2py Code LabWeb2py Code Lab
Web2py Code Lab
Colin Su
 
Web Components With Rails
Web Components With RailsWeb Components With Rails
Web Components With Rails
Boris Nadion
 
Class 12 computer sample paper with answers
Class 12 computer sample paper with answersClass 12 computer sample paper with answers
Class 12 computer sample paper with answers
debarghyamukherjee60
 
Neo4j: Import and Data Modelling
Neo4j: Import and Data ModellingNeo4j: Import and Data Modelling
Neo4j: Import and Data Modelling
Neo4j
 
Pruebas unitarias con django
Pruebas unitarias con djangoPruebas unitarias con django
Pruebas unitarias con django
Tomás Henríquez
 
The Ring programming language version 1.7 book - Part 48 of 196
The Ring programming language version 1.7 book - Part 48 of 196The Ring programming language version 1.7 book - Part 48 of 196
The Ring programming language version 1.7 book - Part 48 of 196
Mahmoud Samir Fayed
 
DataMapper
DataMapperDataMapper
DataMapper
Yehuda Katz
 
Apache Calcite Tutorial - BOSS 21
Apache Calcite Tutorial - BOSS 21Apache Calcite Tutorial - BOSS 21
Apache Calcite Tutorial - BOSS 21
Stamatis Zampetakis
 
ql.io at NodePDX
ql.io at NodePDXql.io at NodePDX
ql.io at NodePDX
Subbu Allamaraju
 
What's new in Python 3.11
What's new in Python 3.11What's new in Python 3.11
What's new in Python 3.11
Henry Schreiner
 
Pemrograman Python untuk Pemula
Pemrograman Python untuk PemulaPemrograman Python untuk Pemula
Pemrograman Python untuk Pemula
Oon Arfiandwi
 
Tools for Solving Performance Issues
Tools for Solving Performance IssuesTools for Solving Performance Issues
Tools for Solving Performance Issues
Odoo
 
The Ring programming language version 1.5 book - Part 8 of 31
The Ring programming language version 1.5 book - Part 8 of 31The Ring programming language version 1.5 book - Part 8 of 31
The Ring programming language version 1.5 book - Part 8 of 31
Mahmoud Samir Fayed
 
Scaling with Python: SF Python Meetup, September 2017
Scaling with Python: SF Python Meetup, September 2017Scaling with Python: SF Python Meetup, September 2017
Scaling with Python: SF Python Meetup, September 2017
Varun Varma
 
Introduction to cython
Introduction to cythonIntroduction to cython
Introduction to cython
John(Qiang) Zhang
 
A Beginner's Guide to Building Data Pipelines with Luigi
A Beginner's Guide to Building Data Pipelines with LuigiA Beginner's Guide to Building Data Pipelines with Luigi
A Beginner's Guide to Building Data Pipelines with Luigi
Growth Intelligence
 
The Ring programming language version 1.5.1 book - Part 43 of 180
The Ring programming language version 1.5.1 book - Part 43 of 180The Ring programming language version 1.5.1 book - Part 43 of 180
The Ring programming language version 1.5.1 book - Part 43 of 180
Mahmoud Samir Fayed
 
Function
FunctionFunction
Function
Sukhdarshan Singh
 
Accessing File-Specific Attributes on Steroids - EuroPython 2008
Accessing File-Specific Attributes on Steroids - EuroPython 2008Accessing File-Specific Attributes on Steroids - EuroPython 2008
Accessing File-Specific Attributes on Steroids - EuroPython 2008
Dinu Gherman
 
Designing REST API automation tests in Kotlin
Designing REST API automation tests in KotlinDesigning REST API automation tests in Kotlin
Designing REST API automation tests in Kotlin
Dmitriy Sobko
 
Web2py Code Lab
Web2py Code LabWeb2py Code Lab
Web2py Code Lab
Colin Su
 
Web Components With Rails
Web Components With RailsWeb Components With Rails
Web Components With Rails
Boris Nadion
 
Class 12 computer sample paper with answers
Class 12 computer sample paper with answersClass 12 computer sample paper with answers
Class 12 computer sample paper with answers
debarghyamukherjee60
 
Neo4j: Import and Data Modelling
Neo4j: Import and Data ModellingNeo4j: Import and Data Modelling
Neo4j: Import and Data Modelling
Neo4j
 
Pruebas unitarias con django
Pruebas unitarias con djangoPruebas unitarias con django
Pruebas unitarias con django
Tomás Henríquez
 
The Ring programming language version 1.7 book - Part 48 of 196
The Ring programming language version 1.7 book - Part 48 of 196The Ring programming language version 1.7 book - Part 48 of 196
The Ring programming language version 1.7 book - Part 48 of 196
Mahmoud Samir Fayed
 
Apache Calcite Tutorial - BOSS 21
Apache Calcite Tutorial - BOSS 21Apache Calcite Tutorial - BOSS 21
Apache Calcite Tutorial - BOSS 21
Stamatis Zampetakis
 
What's new in Python 3.11
What's new in Python 3.11What's new in Python 3.11
What's new in Python 3.11
Henry Schreiner
 
Pemrograman Python untuk Pemula
Pemrograman Python untuk PemulaPemrograman Python untuk Pemula
Pemrograman Python untuk Pemula
Oon Arfiandwi
 
Tools for Solving Performance Issues
Tools for Solving Performance IssuesTools for Solving Performance Issues
Tools for Solving Performance Issues
Odoo
 
The Ring programming language version 1.5 book - Part 8 of 31
The Ring programming language version 1.5 book - Part 8 of 31The Ring programming language version 1.5 book - Part 8 of 31
The Ring programming language version 1.5 book - Part 8 of 31
Mahmoud Samir Fayed
 
Scaling with Python: SF Python Meetup, September 2017
Scaling with Python: SF Python Meetup, September 2017Scaling with Python: SF Python Meetup, September 2017
Scaling with Python: SF Python Meetup, September 2017
Varun Varma
 
A Beginner's Guide to Building Data Pipelines with Luigi
A Beginner's Guide to Building Data Pipelines with LuigiA Beginner's Guide to Building Data Pipelines with Luigi
A Beginner's Guide to Building Data Pipelines with Luigi
Growth Intelligence
 
The Ring programming language version 1.5.1 book - Part 43 of 180
The Ring programming language version 1.5.1 book - Part 43 of 180The Ring programming language version 1.5.1 book - Part 43 of 180
The Ring programming language version 1.5.1 book - Part 43 of 180
Mahmoud Samir Fayed
 
Accessing File-Specific Attributes on Steroids - EuroPython 2008
Accessing File-Specific Attributes on Steroids - EuroPython 2008Accessing File-Specific Attributes on Steroids - EuroPython 2008
Accessing File-Specific Attributes on Steroids - EuroPython 2008
Dinu Gherman
 
Ad

Recently uploaded (20)

Adobe Master Collection CC Crack Advance Version 2025
Adobe Master Collection CC Crack Advance Version 2025Adobe Master Collection CC Crack Advance Version 2025
Adobe Master Collection CC Crack Advance Version 2025
kashifyounis067
 
Avast Premium Security Crack FREE Latest Version 2025
Avast Premium Security Crack FREE Latest Version 2025Avast Premium Security Crack FREE Latest Version 2025
Avast Premium Security Crack FREE Latest Version 2025
mu394968
 
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
AxisTechnolabs
 
Meet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Meet the Agents: How AI Is Learning to Think, Plan, and CollaborateMeet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Meet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Maxim Salnikov
 
Kubernetes_101_Zero_to_Platform_Engineer.pptx
Kubernetes_101_Zero_to_Platform_Engineer.pptxKubernetes_101_Zero_to_Platform_Engineer.pptx
Kubernetes_101_Zero_to_Platform_Engineer.pptx
CloudScouts
 
Download YouTube By Click 2025 Free Full Activated
Download YouTube By Click 2025 Free Full ActivatedDownload YouTube By Click 2025 Free Full Activated
Download YouTube By Click 2025 Free Full Activated
saniamalik72555
 
Who Watches the Watchmen (SciFiDevCon 2025)
Who Watches the Watchmen (SciFiDevCon 2025)Who Watches the Watchmen (SciFiDevCon 2025)
Who Watches the Watchmen (SciFiDevCon 2025)
Allon Mureinik
 
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage DashboardsAdobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
BradBedford3
 
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Andre Hora
 
Adobe After Effects Crack FREE FRESH version 2025
Adobe After Effects Crack FREE FRESH version 2025Adobe After Effects Crack FREE FRESH version 2025
Adobe After Effects Crack FREE FRESH version 2025
kashifyounis067
 
The Significance of Hardware in Information Systems.pdf
The Significance of Hardware in Information Systems.pdfThe Significance of Hardware in Information Systems.pdf
The Significance of Hardware in Information Systems.pdf
drewplanas10
 
Top 10 Client Portal Software Solutions for 2025.docx
Top 10 Client Portal Software Solutions for 2025.docxTop 10 Client Portal Software Solutions for 2025.docx
Top 10 Client Portal Software Solutions for 2025.docx
Portli
 
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New VersionPixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
saimabibi60507
 
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
Egor Kaleynik
 
Douwan Crack 2025 new verson+ License code
Douwan Crack 2025 new verson+ License codeDouwan Crack 2025 new verson+ License code
Douwan Crack 2025 new verson+ License code
aneelaramzan63
 
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
Andre Hora
 
Revolutionizing Residential Wi-Fi PPT.pptx
Revolutionizing Residential Wi-Fi PPT.pptxRevolutionizing Residential Wi-Fi PPT.pptx
Revolutionizing Residential Wi-Fi PPT.pptx
nidhisingh691197
 
Solidworks Crack 2025 latest new + license code
Solidworks Crack 2025 latest new + license codeSolidworks Crack 2025 latest new + license code
Solidworks Crack 2025 latest new + license code
aneelaramzan63
 
EASEUS Partition Master Crack + License Code
EASEUS Partition Master Crack + License CodeEASEUS Partition Master Crack + License Code
EASEUS Partition Master Crack + License Code
aneelaramzan63
 
Scaling GraphRAG: Efficient Knowledge Retrieval for Enterprise AI
Scaling GraphRAG:  Efficient Knowledge Retrieval for Enterprise AIScaling GraphRAG:  Efficient Knowledge Retrieval for Enterprise AI
Scaling GraphRAG: Efficient Knowledge Retrieval for Enterprise AI
danshalev
 
Adobe Master Collection CC Crack Advance Version 2025
Adobe Master Collection CC Crack Advance Version 2025Adobe Master Collection CC Crack Advance Version 2025
Adobe Master Collection CC Crack Advance Version 2025
kashifyounis067
 
Avast Premium Security Crack FREE Latest Version 2025
Avast Premium Security Crack FREE Latest Version 2025Avast Premium Security Crack FREE Latest Version 2025
Avast Premium Security Crack FREE Latest Version 2025
mu394968
 
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
Interactive odoo dashboards for sales, CRM , Inventory, Invoice, Purchase, Pr...
AxisTechnolabs
 
Meet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Meet the Agents: How AI Is Learning to Think, Plan, and CollaborateMeet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Meet the Agents: How AI Is Learning to Think, Plan, and Collaborate
Maxim Salnikov
 
Kubernetes_101_Zero_to_Platform_Engineer.pptx
Kubernetes_101_Zero_to_Platform_Engineer.pptxKubernetes_101_Zero_to_Platform_Engineer.pptx
Kubernetes_101_Zero_to_Platform_Engineer.pptx
CloudScouts
 
Download YouTube By Click 2025 Free Full Activated
Download YouTube By Click 2025 Free Full ActivatedDownload YouTube By Click 2025 Free Full Activated
Download YouTube By Click 2025 Free Full Activated
saniamalik72555
 
Who Watches the Watchmen (SciFiDevCon 2025)
Who Watches the Watchmen (SciFiDevCon 2025)Who Watches the Watchmen (SciFiDevCon 2025)
Who Watches the Watchmen (SciFiDevCon 2025)
Allon Mureinik
 
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage DashboardsAdobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
Adobe Marketo Engage Champion Deep Dive - SFDC CRM Synch V2 & Usage Dashboards
BradBedford3
 
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Exceptional Behaviors: How Frequently Are They Tested? (AST 2025)
Andre Hora
 
Adobe After Effects Crack FREE FRESH version 2025
Adobe After Effects Crack FREE FRESH version 2025Adobe After Effects Crack FREE FRESH version 2025
Adobe After Effects Crack FREE FRESH version 2025
kashifyounis067
 
The Significance of Hardware in Information Systems.pdf
The Significance of Hardware in Information Systems.pdfThe Significance of Hardware in Information Systems.pdf
The Significance of Hardware in Information Systems.pdf
drewplanas10
 
Top 10 Client Portal Software Solutions for 2025.docx
Top 10 Client Portal Software Solutions for 2025.docxTop 10 Client Portal Software Solutions for 2025.docx
Top 10 Client Portal Software Solutions for 2025.docx
Portli
 
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New VersionPixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
Pixologic ZBrush Crack Plus Activation Key [Latest 2025] New Version
saimabibi60507
 
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
How Valletta helped healthcare SaaS to transform QA and compliance to grow wi...
Egor Kaleynik
 
Douwan Crack 2025 new verson+ License code
Douwan Crack 2025 new verson+ License codeDouwan Crack 2025 new verson+ License code
Douwan Crack 2025 new verson+ License code
aneelaramzan63
 
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
What Do Contribution Guidelines Say About Software Testing? (MSR 2025)
Andre Hora
 
Revolutionizing Residential Wi-Fi PPT.pptx
Revolutionizing Residential Wi-Fi PPT.pptxRevolutionizing Residential Wi-Fi PPT.pptx
Revolutionizing Residential Wi-Fi PPT.pptx
nidhisingh691197
 
Solidworks Crack 2025 latest new + license code
Solidworks Crack 2025 latest new + license codeSolidworks Crack 2025 latest new + license code
Solidworks Crack 2025 latest new + license code
aneelaramzan63
 
EASEUS Partition Master Crack + License Code
EASEUS Partition Master Crack + License CodeEASEUS Partition Master Crack + License Code
EASEUS Partition Master Crack + License Code
aneelaramzan63
 
Scaling GraphRAG: Efficient Knowledge Retrieval for Enterprise AI
Scaling GraphRAG:  Efficient Knowledge Retrieval for Enterprise AIScaling GraphRAG:  Efficient Knowledge Retrieval for Enterprise AI
Scaling GraphRAG: Efficient Knowledge Retrieval for Enterprise AI
danshalev
 
Ad

Mock Hell PyCon DE and PyData Berlin 2019

  • 1. Mock Hell PyConDE and PyData Berlin, 2019 [email protected] https://www.linkedin.com/in/junged/ twitter: @ejjcatx
  • 2. •Backend Engineer @ Quid •Python => 1st dynamic language [email protected] https://www.linkedin.com/in/junged/ twitter: @ejjcatx
  • 5. Perfect isolation, 100% coverage Tests that test nothing Reverse- engineering mocks with the debugger Mocks that prevent refactoring Lust Gluttony Greed Fraud Treachery Heresy Confusing patch targets Brittle assertions Complex Setup Deep/recursive mocks; mocks-in- mocks Too many mocks or patches Limbo
  • 7. # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 8. # test_core.py from core import total_value def test_total_value(): assert 300. == total_value(“[email protected]”, 92) # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 9. # test_core.py from unittest.mock import patch, call from core import total_value def test_total_value(): with patch('core.db_read') as mock_read: assert 300. == total_value(“[email protected]”, 92) # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 10. # test_core.py from unittest.mock import patch, call from core import total_value def test_total_value(): with patch('core.db_read') as mock_read: mock_read.return_value = [100, 200] assert 300. == total_value(“[email protected]”, 92) # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 11. # test_core.py from unittest.mock import patch, call from core import total_value def test_total_value(): with patch(‘core.db_read’) as mock_read: mock_read.return_value = [100, 200] assert 300. == total_value(“[email protected]”, 92) assert [call(“[email protected]", inv_no=92)] == mock_read.calls # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 19. News Scraper Internet S3 You have a large corpus of scraped public news articles stored in S3, for use in NLP experiments.
  • 21. Market Movers Data as of Jul 19 Friday’s Close: Dow-68.77 27,154.20 -0.25%Nasdaq-60.754 2,976.61 -0.62% S&P 500 | S&P 1500 Most Actives Company Price Change % Change MSFT Microsoft Corp 136.62 +0.20 +0.15% BAC Bank of America Corp 29.40 -0.08-0.27% … MU Micron Technology Inc 45.52 +0.85 +1.90% F Ford Motor Co 10.20 -0.06-0.58% GE General Electric Co 10.04 -0.02-0.20% T AT&T Inc 32.79 -0.30-0.91% … FCX Freeport-McMoRan Inc 11.49 +0.34 +3.05% INTC Intel Corp 50.27 +0.33 +0.66% PFE Pfizer Inc 42.77 -0.29-0.67% >50% lines stock symbols
  • 22. Write a script that scans your news archive to check for stock quotes articles.
  • 24. S3 • To scan everything in a bucket: • Get a paginated list of keys • Download each object by key
  • 25. { "Name": “my_bucket”, "Prefix": "news", "MaxKeys": 1000, "Contents": [ { "Key": "news/", "LastModified": "2019-07-21", "Size": 0 }, { "Key": "news/1.txt", "LastModified": "2019-07-21", "Size": 12695 }, ... { "Key": "news/3.txt", "LastModified": "2019-07-21", "Size": 1587 } ], ... }
  • 26. ├── newscheck │   ├── __init__.py │   ├── config.py │   ├── stock.py │   ├── core.py │   ├── util.py │   └── test_core.py ├── script.py
  • 27. ├── newscheck │   ├── __init__.py │   ├── config.py │   ├── stock.py │   ├── core.py │   ├── util.py │   └── test_core.py ├── script.py
  • 28. # script.py from newscheck.core import find_bad_articles if __name__ == "__main__": print(list(find_bad_articles(“my_bucket”)))
  • 29. ├── newscheck │   ├── __init__.py │   ├── config.py │   ├── stock.py │   ├── core.py │   ├── util.py │   └── test_core.py ├── script.py
  • 30. # core.py import boto3 import newscheck.config as config import newscheck.stock as stock from newscheck.util import * def find_bad_articles(bucket_name): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS): yield key
  • 31. ├── newscheck │   ├── __init__.py │   ├── config.py │   ├── stock.py │   ├── core.py │   ├── util.py │   └── test_core.py ├── script.py
  • 32. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 33. # util.py import newscheck.config as config def get_pages(client, bucket_name, prefix): paginator = client.get_paginator('list_objects') page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix) return page_iterator +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 34. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 35. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- def keys_from(page): for item in page['Contents']: key = item['Key'] if key == f"{config.PREFIX}/": #ignore root continue yield key +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): —
  • 36. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 37. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): --- def get_file(client, bucket_name, key): buf = io.BytesIO() client.download_fileobj(bucket_name, key, buf) # Decode and drop empty lines buf.seek(0) lines = (line.decode().strip() for line in buf.readlines()) return list(filter(lambda x:x, lines)) +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 38. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 39. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): — def check_article(lines, threshold, stock_symbols): limit = threshold * len(lines) while lines and limit: line = lines.pop() first_word = line.split()[0] if first_word in stock_symbols: limit -= 1 return limit <= 0
  • 40. # util.py import newscheck.config as config +---- 4 lines: def get_pages(client, bucket_name, prefix): --- +---- 6 lines: def keys_from(page): +---- 7 lines: def get_file(client, bucket_name, key): --- +--- 10 lines: def check_article(lines, threshold, stock_symbols): ---
  • 41. ├── newscheck │   ├── __init__.py │   ├── config.py │   ├── stock.py │   ├── core.py │   ├── util.py │   └── test_core.py ├── script.py
  • 42. # test_core.py from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): # Test something here!
  • 43. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub
  • 44. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs
  • 45. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles)
  • 46. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls )
  • 47. import boto3 import newscheck.config as config import newscheck.stock as stock from newscheck.util import * def find_bad_articles(bucket_name): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS): yield key Changing Imports?
  • 48. import boto3.client as s3client import newscheck.config as config import newscheck.stock as stock from newscheck.util import * def find_bad_articles(bucket_name): client = s3client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS): yield key Changing Imports?
  • 49. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls ) Breaks this patch
  • 50. import boto3 import newscheck.config as config import newscheck.stock as stock from newscheck.util import * def find_bad_articles(bucket_name): client = boto3.client(‘s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS): yield key Args to Kwargs?
  • 51. import boto3 import newscheck.config as config import newscheck.stock as stock from newscheck.util import * def find_bad_articles(bucket_name): client = boto3.client(‘s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name=bucket_name, prefix=config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key=key) if check_article(lines, config.THRESHOLD, stock.TICKER_SYMBOLS): yield key Args to Kwargs?
  • 52. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls ) Breaks these assertions
  • 53. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls ) Missing an assertion
  • 54. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls ) What if you want to support tarballs instead of S3?
  • 55. from unittest.mock import MagicMock, call, patch from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): @patch("newscheck.core.get_file") @patch("newscheck.core.get_pages") @patch("newscheck.core.boto3.client") def test_find_bad_articles(self, client_mock, get_pages_mock, get_file_mock): page_stub = [{ "Contents": [ {"Key": 'news/'}, {"Key": 'news/good.txt'}, {"Key": 'news/bad.txt'}] }] get_pages_mock.return_value = page_stub file_stubs = [ "lorem ipsum dolorumn foo baz bar bizn hello world".split('n'), "MSFT 99.32n BAC 22.3n F 33.2n".split('n'), ] get_file_mock.side_effect = file_stubs bad_articles = list(find_bad_articles("my_bucket")) self.assertEqual(["news/bad.txt"], bad_articles) self.assertEqual([ call(client_mock.return_value, "my_bucket", "news/good.txt"), call(client_mock.return_value, "my_bucket", "news/bad.txt") ], get_file_mock.mock_calls ) What if you want different checking logic?
  • 58. Mocking is for: •test isolation •simulating dependencies •avoiding side-effects What went wrong?
  • 59. Mocking is for: •test isolation •simulating dependencies •avoiding side-effects (Be careful about overmocking!) What went wrong?
  • 60. Mocking is for: •test isolation •simulating dependencies •avoiding side-effects (Be careful about overmocking!) What went wrong?
  • 63. Mock Objects is an extension to Test Driven Development that supports good Object-Oriented design …
  • 64. It[’s] less interesting as a technique for isolating tests … than is widely thought.
  • 65. What went wrong. Mocking is supposed to be a technique for (1) Object Oriented Design, in the context of (2) TDD, for exploratory design and discovery. Mocking is not just a tool for test isolation.
  • 66. What went wrong. Mocking is supposed to be a technique for (1) Object Oriented Design, in the context of (2) TDD, for exploratory design and discovery. Mocking is not just a tool for test isolation.
  • 68. Freeman and Pryce advocate what many call “the London school of TDD, …” I’m not fully disagreeing …  but for maintainability … tests using mocks creates more trouble than it’s worth.
  • 70. “Mocks aren’t Stubs” A mock is a kind of test double. Other kinds of test doubles include stubs, fakes, spies, dummies.
  • 71. “Mocks aren’t Stubs” A mock is a kind of test double. Other kinds of test doubles include stubs, fakes, spies, dummies. The difference between different “schools” of TDD, is how you use test doubles.
  • 72. from unittest.mock import patch, call from my_module import total_value def test_total_value(): with patch('my_module.db_read') as mock_read: mock_read.return_value = [100, 200] assert 300. == total_value(“[email protected]”, 92) assert [call(“[email protected]”, inv_no=92)] == mock_read.calls Option: Patch a Mock
  • 73. from unittest.mock import patch, call from my_module import total_value def test_total_value(): with patch('my_module.db_read') as mock_read: mock_read.return_value = [100, 200] assert 300. == total_value(“[email protected]”, 92) assert [call(“[email protected]”, inv_no=92)] == mock_read.calls stub returns canned values Option: Patch a Mock
  • 74. from unittest.mock import patch, call from my_module import total_value def test_total_value(): with patch('my_module.db_read') as mock_read: mock_read.return_value = [100, 200] assert 300. == total_value(“[email protected]”, 92) assert [call(“[email protected]”, inv_no=92)] == mock_read.calls mock validates interaction stub returns canned values Option: Patch a Mock
  • 75. Option: Inject a Mock # core.py from db import db_read def total_value(email, invoice_id, discount=0.0): items = db_read(email, inv_no=invoice_id) return sum(items)*(1.0-discount)
  • 76. # core.py def total_value(read, email, invoice_id, discount=0.0): items = read(email, inv_no=invoice_id) return sum(items)*(1.0-discount) Option: Inject a Mock
  • 77. def test_total_value(): assert 300 == total_value(???, “[email protected]”, 92) Option: Inject a Mock
  • 78. Option: Inject a Mock def test_total_value(): mock_read = MagicMock() mock_read.return_value = [100, 200] assert 300 == total_value(???, “[email protected]”, 92)
  • 79. Option: Inject a Mock def test_total_value(): mock_read = MagicMock() mock_read.return_value = [100, 200] assert 300 == total_value(mock_read, “[email protected]”, 92) assert [call(“[email protected]”, inv_no=92)] == mock_read.calls
  • 80. def test_total_value(): assert 300 == total_value(???, “[email protected]”, 92) Option: Inject a Fake
  • 81. def test_total_value(): def fake_db_read(email, invoice_id): with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj: return json.load(fobj) assert 300 == total_value(???, “[email protected]”, 92) Option: Inject a Fake
  • 82. def test_total_value(): def fake_db_read(email, invoice_id): with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj: return json.load(fobj) assert 300 == total_value(fake_db_read, “[email protected]”, 92) Option: Inject a Fake
  • 83. def test_total_value(): def fake_db_read(email, invoice_id): with open(f’./fixtures/{email}/{invoice_id}.json’) as fobj: return json.load(fobj) assert 300 == total_value(fake_db_read, “[email protected]”, 92) def test_total_value(): mock_read = MagicMock() mock_read.return_value = [100, 200] assert 300. == total_value(mock_read, “[email protected]”, 92) assert [call(“[email protected]”, inv_no=92)] == mock_read.calls Mockist Classical
  • 94. def find_bad_articles(bucket_name): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS): yield key if __name__ == "__main__": print(list(find_bad_articles(“my_bucket”))) To decouple check_article
  • 95. Inject a Predicate def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key if __name__ == "__main__": print(list(find_bad_articles(“my_bucket”, predicate=???)))
  • 96. Using a Closure if __name__ == "__main__": def is_stock_quotes(lines): return check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS) print(list(find_bad_articles(“my_bucket”, is_stock_quotes)))
  • 97. “Dependency Injection is just passing things in.”
  • 98. “Dependency Injection is just passing things in.”
  • 99. What do you inject? def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 100. boto3? def find_bad_articles(bucket_name, predicate, boto3): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 101. client? def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 102. client? def find_bad_articles(bucket_name, predicate, client): for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 103. get_pages, get_file…? def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 104. But what about keys_from? def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 105. Remember: Dependency Injection and Mocking are Object Oriented Design
  • 112. find bad keys from get pages script get file Archive S3 Archive “Detail depends on Abstraction” Dependency Inversion depends on pred- icate
  • 113. Extract def find_bad_articles(bucket_name, predicate): client = boto3.client('s3', region_name=config.REGION_NAME) for page in get_pages(client, bucket_name, config.PREFIX): for key in keys_from(page): lines = get_file(client, bucket_name, key) if predicate(lines): yield key
  • 114. def find_bad_articles(archive, predicate): for page in archive.get_pages(): for key in archive.keys_from(page): lines = archive.get_file(key) if predicate(lines): yield key Extract
  • 115. Encapsulate class S3Archive(object): def __init__(self, bucket_name, prefix, region_name): """Ctor""" self.client = boto3.client('s3', region_name=region_name) self.bucket_name = bucket_name self.prefix = prefix +---- 7 lines: def get_file(self, key): -------------------------- +---- 4 lines: def get_pages(self): ------------------------------ +---- 6 lines: def keys_from(self, page): ------------------------
  • 116. Inject in App if __name__ == "__main__": def is_stock_quotes(lines): return check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS) print(list(find_bad_articles(???, is_stock_quotes)))
  • 117. Inject in App if __name__ == "__main__": def is_stock_quotes(lines): return check_article(lines, config.THRESHOLD, config.STOCK_SYMBOLS) archive = S3Archive(“my_bucket”, config.PREFIX, config.REGION_NAME) print(list(find_bad_articles(archive, is_stock_quotes)))
  • 118. def find_bad_articles(archive, predicate): for page in archive.get_pages(): for key in archive.keys_from(page): lines = archive.get_file(key) if predicate(lines): yield key Who should be responsible for Paging and Key Parsing?
  • 119. def find_bad_articles(archive, predicate): for key in archive.get_keys(): lines = archive.get_file(key) if predicate(lines): yield key Delegate Responsibility to Archive
  • 120. class S3Archive(object): def __init__(self, bucket_name, prefix, region_name): """Ctor""" self.client = boto3.client('s3', region_name=region_name) self.bucket_name = bucket_name self.prefix = prefix def get_keys(self): """Get an iterable of keys""" for page in self.get_pages(): for key in self.keys_from(page): yield key +---- 7 lines: def get_file(self, key): -------------------------- +---- 4 lines: def get_pages(self): ------------------------------ +---- 6 lines: def keys_from(self, page): ------------------------ Delegate Responsibility to Archive
  • 121. def find_bad_articles(archive, predicate): for key in archive.get_keys(): lines = archive.get_file(key) if predicate(lines): yield key
  • 122. def find_bad_articles(archive, predicate): for key in archive.get_keys(): lines = archive.get_file(key) if predicate(lines): yield key KafkaArchive?
  • 123. Kafka Stream of Tuples “news/2.txt”, “lorem ispum dolorumn …” “news/1.txt”, “quot et foo bar det in n …” “news/3.txt”, “foos pis o lot forum box n …” “news/9.txt”, “MSFT 99.34 n F 33.34 …”
  • 124. def find_bad_articles(archive, predicate): for key in archive.get_keys(): lines = archive.get_file(key) if predicate(lines): yield key
  • 125. def find_bad_articles(archive, predicate): for key in archive.get_keys(): lines = archive.get_file(key) if predicate(lines): yield key KafkaArchive leads to Liskov Violation
  • 126. class S3Archive(object): def __init__(self, bucket_name, prefix, client): """Ctor""" self.client = boto3.client('s3', region_name=region_name) self.bucket_name = bucket_name self.prefix = prefix def items(self): for key in self.get_keys(): lines = self.get_file(key) yield key, lines def get_keys(self): """Get an iterable of keys""" for page in self.get_pages(): for key in self.keys_from(page): yield key +---- 7 lines: def get_file(self, key): -------------------------- +---- 4 lines: def get_pages(self): ------------------------------ +---- 6 lines: def keys_from(self, page): ------------------------ Hide Responsibility in Archive
  • 127. def find_bad_articles(archive, predicate): for key, lines in archive.items(): if predicate(lines): yield key Archive as a Tuple Stream
  • 128. import unittest from newscheck.core import find_bad_articles class TestBadArticleFinder(unittest.TestCase): def test_find_bad_articles(self): data = { "good.txt": ["lorem ipsum dolorum"], "bad.txt": ["i am the bad article"] } def check(lines): return lines[0] == "i am the bad article" bad_articles = list(find_bad_articles(data.items(), check)) self.assertEqual(1, len(bad_articles)) self.assertEqual("bad.txt", bad_articles[0]) Test
  • 130. [email protected] https://www.linkedin.com/in/junged/ twitter: @ejjcatx Mocks aren’t Stubs (Fowler) Clean Code (Bob Martin) Growing Object-Oriented Software, Guided by Tests (Pryce, Freeman) The Art of Unit Testing (Osherove)