Simplify mocking in tests

This commit is contained in:
Ivan Habunek 2018-06-07 10:00:50 +02:00
parent 7a38c5704d
commit 9f23ba4d55
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
3 changed files with 176 additions and 243 deletions

View File

@ -1,31 +1,32 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest import pytest
from requests import Request from unittest import mock
from toot import App, CLIENT_NAME, CLIENT_WEBSITE from toot import App, CLIENT_NAME, CLIENT_WEBSITE
from toot.api import create_app, login, SCOPES, AuthenticationError from toot.api import create_app, login, SCOPES, AuthenticationError
from tests.utils import MockResponse, Expectations from tests.utils import MockResponse
def test_create_app(monkeypatch): @mock.patch('toot.http.anon_post')
request = Request('POST', 'https://bigfish.software/api/v1/apps', def test_create_app(mock_post):
data={'website': CLIENT_WEBSITE, mock_post.return_value = MockResponse({
'client_name': CLIENT_NAME, 'client_id': 'foo',
'scopes': SCOPES, 'client_secret': 'bar',
'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob'}) })
response = MockResponse({'client_id': 'foo',
'client_secret': 'bar'})
e = Expectations()
e.add(request, response)
e.patch(monkeypatch)
create_app('bigfish.software') create_app('bigfish.software')
mock_post.assert_called_once_with('https://bigfish.software/api/v1/apps', {
'website': CLIENT_WEBSITE,
'client_name': CLIENT_NAME,
'scopes': SCOPES,
'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob',
})
def test_login(monkeypatch):
@mock.patch('toot.http.anon_post')
def test_login(mock_post):
app = App('bigfish.software', 'https://bigfish.software', 'foo', 'bar') app = App('bigfish.software', 'https://bigfish.software', 'foo', 'bar')
data = { data = {
@ -37,23 +38,21 @@ def test_login(monkeypatch):
'scope': SCOPES, 'scope': SCOPES,
} }
request = Request('POST', 'https://bigfish.software/oauth/token', data=data) mock_post.return_value = MockResponse({
response = MockResponse({
'token_type': 'bearer', 'token_type': 'bearer',
'scope': 'read write follow', 'scope': 'read write follow',
'access_token': 'xxx', 'access_token': 'xxx',
'created_at': 1492523699 'created_at': 1492523699
}) })
e = Expectations()
e.add(request, response)
e.patch(monkeypatch)
login(app, 'user', 'pass') login(app, 'user', 'pass')
mock_post.assert_called_once_with(
'https://bigfish.software/oauth/token', data, allow_redirects=False)
def test_login_failed(monkeypatch):
@mock.patch('toot.http.anon_post')
def test_login_failed(mock_post):
app = App('bigfish.software', 'https://bigfish.software', 'foo', 'bar') app = App('bigfish.software', 'https://bigfish.software', 'foo', 'bar')
data = { data = {
@ -65,12 +64,10 @@ def test_login_failed(monkeypatch):
'scope': SCOPES, 'scope': SCOPES,
} }
request = Request('POST', 'https://bigfish.software/oauth/token', data=data) mock_post.return_value = MockResponse(is_redirect=True)
response = MockResponse(is_redirect=True)
e = Expectations()
e.add(request, response)
e.patch(monkeypatch)
with pytest.raises(AuthenticationError): with pytest.raises(AuthenticationError):
login(app, 'user', 'pass') login(app, 'user', 'pass')
mock_post.assert_called_once_with(
'https://bigfish.software/oauth/token', data, allow_redirects=False)

View File

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import io
import pytest import pytest
import requests
import re import re
from requests import Request from unittest import mock
from toot import config, console, User, App from toot import console, User, App, http
from toot.exceptions import ConsoleError from toot.exceptions import ConsoleError
from tests.utils import MockResponse, Expectations from tests.utils import MockResponse
app = App('habunek.com', 'https://habunek.com', 'foo', 'bar') app = App('habunek.com', 'https://habunek.com', 'foo', 'bar')
user = User('habunek.com', 'ivan@habunek.com', 'xxx') user = User('habunek.com', 'ivan@habunek.com', 'xxx')
@ -25,59 +25,49 @@ def test_print_usage(capsys):
assert "toot - a Mastodon CLI client" in out assert "toot - a Mastodon CLI client" in out
def test_post_defaults(monkeypatch, capsys): @mock.patch('toot.http.post')
def mock_prepare(request): def test_post_defaults(mock_post, capsys):
assert request.method == 'POST' mock_post.return_value = MockResponse({
assert request.url == 'https://habunek.com/api/v1/statuses' 'url': 'https://habunek.com/@ihabunek/1234567890'
assert request.headers == {'Authorization': 'Bearer xxx'} })
assert request.data == {
'status': 'Hello world',
'visibility': 'public',
'media_ids[]': None,
}
def mock_send(*args, **kwargs):
return MockResponse({
'url': 'http://ivan.habunek.com/'
})
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
console.run_command(app, user, 'post', ['Hello world']) console.run_command(app, user, 'post', ['Hello world'])
mock_post.assert_called_once_with(app, user, '/api/v1/statuses', {
'status': 'Hello world',
'visibility': 'public',
'media_ids[]': None,
})
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "Toot posted" in out assert 'Toot posted' in out
assert 'https://habunek.com/@ihabunek/1234567890' in out
assert not err
def test_post_with_options(monkeypatch, capsys): @mock.patch('toot.http.post')
def mock_prepare(request): def test_post_with_options(mock_post, capsys):
assert request.method == 'POST' args = ['Hello world', '--visibility', 'unlisted']
assert request.url == 'https://habunek.com/api/v1/statuses'
assert request.headers == {'Authorization': 'Bearer xxx'}
assert request.data == {
'status': '"Hello world"',
'visibility': 'unlisted',
'media_ids[]': None,
}
def mock_send(*args, **kwargs): mock_post.return_value = MockResponse({
return MockResponse({ 'url': 'https://habunek.com/@ihabunek/1234567890'
'url': 'http://ivan.habunek.com/' })
})
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
args = ['"Hello world"', '--visibility', 'unlisted']
console.run_command(app, user, 'post', args) console.run_command(app, user, 'post', args)
mock_post.assert_called_once_with(app, user, '/api/v1/statuses', {
'status': 'Hello world',
'media_ids[]': None,
'visibility': 'unlisted',
})
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "Toot posted" in out assert 'Toot posted' in out
assert 'https://habunek.com/@ihabunek/1234567890' in out
assert not err
def test_post_invalid_visibility(monkeypatch, capsys): def test_post_invalid_visibility(capsys):
args = ['Hello world', '--visibility', 'foo'] args = ['Hello world', '--visibility', 'foo']
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -87,7 +77,7 @@ def test_post_invalid_visibility(monkeypatch, capsys):
assert "invalid visibility value: 'foo'" in err assert "invalid visibility value: 'foo'" in err
def test_post_invalid_media(monkeypatch, capsys): def test_post_invalid_media(capsys):
args = ['Hello world', '--media', 'does_not_exist.jpg'] args = ['Hello world', '--media', 'does_not_exist.jpg']
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -97,86 +87,71 @@ def test_post_invalid_media(monkeypatch, capsys):
assert "can't open 'does_not_exist.jpg'" in err assert "can't open 'does_not_exist.jpg'" in err
def test_timeline(monkeypatch, capsys): @mock.patch('toot.http.get')
def mock_prepare(request): def test_timeline(mock_get, monkeypatch, capsys):
assert request.url == 'https://habunek.com/api/v1/timelines/home' mock_get.return_value = MockResponse([{
assert request.headers == {'Authorization': 'Bearer xxx'} 'account': {
assert request.params == {} 'display_name': 'Frank Zappa',
'username': 'fz'
def mock_send(*args, **kwargs): },
return MockResponse([{ 'created_at': '2017-04-12T15:53:18.174Z',
'account': { 'content': "<p>The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.</p>",
'display_name': 'Frank Zappa', 'reblog': None,
'username': 'fz' }])
},
'created_at': '2017-04-12T15:53:18.174Z',
'content': "<p>The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.</p>",
'reblog': None,
}])
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
console.run_command(app, user, 'timeline', []) console.run_command(app, user, 'timeline', [])
mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home')
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "The computer can't tell you the emotional story." in out assert "The computer can't tell you the emotional story." in out
assert "Frank Zappa @fz" in out assert "Frank Zappa @fz" in out
def test_upload(monkeypatch, capsys): @mock.patch('toot.http.post')
def mock_prepare(request): def test_upload(mock_post, capsys):
assert request.method == 'POST' mock_post.return_value = MockResponse({
assert request.url == 'https://habunek.com/api/v1/media' 'id': 123,
assert request.headers == {'Authorization': 'Bearer xxx'} 'url': 'https://bigfish.software/123/456',
assert request.files.get('file') is not None 'preview_url': 'https://bigfish.software/789/012',
'text_url': 'https://bigfish.software/345/678',
def mock_send(*args, **kwargs): 'type': 'image',
return MockResponse({ })
'id': 123,
'url': 'https://bigfish.software/123/456',
'preview_url': 'https://bigfish.software/789/012',
'text_url': 'https://bigfish.software/345/678',
'type': 'image',
})
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
console.run_command(app, user, 'upload', [__file__]) console.run_command(app, user, 'upload', [__file__])
mock_post.assert_called_once()
args, kwargs = http.post.call_args
assert args == (app, user, '/api/v1/media')
assert isinstance(kwargs['files']['file'], io.BufferedReader)
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "Uploading media" in out assert "Uploading media" in out
assert __file__ in out assert __file__ in out
def test_search(monkeypatch, capsys): @mock.patch('toot.http.get')
def mock_prepare(request): def test_search(mock_get, capsys):
assert request.url == 'https://habunek.com/api/v1/search' mock_get.return_value = MockResponse({
assert request.headers == {'Authorization': 'Bearer xxx'} 'hashtags': ['foo', 'bar', 'baz'],
assert request.params == { 'accounts': [{
'q': 'freddy', 'acct': 'thequeen',
'resolve': False, 'display_name': 'Freddy Mercury'
} }, {
'acct': 'thequeen@other.instance',
def mock_send(*args, **kwargs): 'display_name': 'Mercury Freddy'
return MockResponse({ }],
'hashtags': ['foo', 'bar', 'baz'], 'statuses': [],
'accounts': [{ })
'acct': 'thequeen',
'display_name': 'Freddy Mercury'
}, {
'acct': 'thequeen@other.instance',
'display_name': 'Mercury Freddy'
}],
'statuses': [],
})
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
console.run_command(app, user, 'search', ['freddy']) console.run_command(app, user, 'search', ['freddy'])
mock_get.assert_called_once_with(app, user, '/api/v1/search', {
'q': 'freddy',
'resolve': False,
})
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "Hashtags:\n\033[32m#foo\033[0m, \033[32m#bar\033[0m, \033[32m#baz\033[0m" in out assert "Hashtags:\n\033[32m#foo\033[0m, \033[32m#bar\033[0m, \033[32m#baz\033[0m" in out
assert "Accounts:" in out assert "Accounts:" in out
@ -184,81 +159,69 @@ def test_search(monkeypatch, capsys):
assert "\033[32m@thequeen@other.instance\033[0m Mercury Freddy" in out assert "\033[32m@thequeen@other.instance\033[0m Mercury Freddy" in out
def test_follow(monkeypatch, capsys): @mock.patch('toot.http.post')
req1 = Request('GET', 'https://habunek.com/api/v1/accounts/search', @mock.patch('toot.http.get')
params={'q': 'blixa'}, def test_follow(mock_get, mock_post, capsys):
headers={'Authorization': 'Bearer xxx'}) mock_get.return_value = MockResponse([
res1 = MockResponse([
{'id': 123, 'acct': 'blixa@other.acc'}, {'id': 123, 'acct': 'blixa@other.acc'},
{'id': 321, 'acct': 'blixa'}, {'id': 321, 'acct': 'blixa'},
]) ])
mock_post.return_value = MockResponse()
req2 = Request('POST', 'https://habunek.com/api/v1/accounts/321/follow',
headers={'Authorization': 'Bearer xxx'})
res2 = MockResponse()
expectations = Expectations([req1, req2], [res1, res2])
expectations.patch(monkeypatch)
console.run_command(app, user, 'follow', ['blixa']) console.run_command(app, user, 'follow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v1/accounts/search', {'q': 'blixa'})
mock_post.assert_called_once_with(app, user, '/api/v1/accounts/321/follow')
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "You are now following blixa" in out assert "You are now following blixa" in out
def test_follow_not_found(monkeypatch, capsys): @mock.patch('toot.http.get')
req = Request('GET', 'https://habunek.com/api/v1/accounts/search', def test_follow_not_found(mock_get, capsys):
params={'q': 'blixa'}, headers={'Authorization': 'Bearer xxx'}) mock_get.return_value = MockResponse()
res = MockResponse()
expectations = Expectations([req], [res])
expectations.patch(monkeypatch)
with pytest.raises(ConsoleError) as ex: with pytest.raises(ConsoleError) as ex:
console.run_command(app, user, 'follow', ['blixa']) console.run_command(app, user, 'follow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v1/accounts/search', {'q': 'blixa'})
assert "Account not found" == str(ex.value) assert "Account not found" == str(ex.value)
def test_unfollow(monkeypatch, capsys): @mock.patch('toot.http.post')
req1 = Request('GET', 'https://habunek.com/api/v1/accounts/search', @mock.patch('toot.http.get')
params={'q': 'blixa'}, def test_unfollow(mock_get, mock_post, capsys):
headers={'Authorization': 'Bearer xxx'}) mock_get.return_value = MockResponse([
res1 = MockResponse([
{'id': 123, 'acct': 'blixa@other.acc'}, {'id': 123, 'acct': 'blixa@other.acc'},
{'id': 321, 'acct': 'blixa'}, {'id': 321, 'acct': 'blixa'},
]) ])
req2 = Request('POST', 'https://habunek.com/api/v1/accounts/321/unfollow', mock_post.return_value = MockResponse()
headers={'Authorization': 'Bearer xxx'})
res2 = MockResponse()
expectations = Expectations([req1, req2], [res1, res2])
expectations.patch(monkeypatch)
console.run_command(app, user, 'unfollow', ['blixa']) console.run_command(app, user, 'unfollow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v1/accounts/search', {'q': 'blixa'})
mock_post.assert_called_once_with(app, user, '/api/v1/accounts/321/unfollow')
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "You are no longer following blixa" in out assert "You are no longer following blixa" in out
def test_unfollow_not_found(monkeypatch, capsys): @mock.patch('toot.http.get')
req = Request('GET', 'https://habunek.com/api/v1/accounts/search', def test_unfollow_not_found(mock_get, capsys):
params={'q': 'blixa'}, headers={'Authorization': 'Bearer xxx'}) mock_get.return_value = MockResponse([])
res = MockResponse([])
expectations = Expectations([req], [res])
expectations.patch(monkeypatch)
with pytest.raises(ConsoleError) as ex: with pytest.raises(ConsoleError) as ex:
console.run_command(app, user, 'unfollow', ['blixa']) console.run_command(app, user, 'unfollow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v1/accounts/search', {'q': 'blixa'})
assert "Account not found" == str(ex.value) assert "Account not found" == str(ex.value)
def test_whoami(monkeypatch, capsys): @mock.patch('toot.http.get')
req = Request('GET', 'https://habunek.com/api/v1/accounts/verify_credentials', def test_whoami(mock_get, capsys):
headers={'Authorization': 'Bearer xxx'}) mock_get.return_value = MockResponse({
res = MockResponse({
'acct': 'ihabunek', 'acct': 'ihabunek',
'avatar': 'https://files.mastodon.social/accounts/avatars/000/046/103/original/6a1304e135cac514.jpg?1491312434', 'avatar': 'https://files.mastodon.social/accounts/avatars/000/046/103/original/6a1304e135cac514.jpg?1491312434',
'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/046/103/original/6a1304e135cac514.jpg?1491312434', 'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/046/103/original/6a1304e135cac514.jpg?1491312434',
@ -276,11 +239,10 @@ def test_whoami(monkeypatch, capsys):
'username': 'ihabunek' 'username': 'ihabunek'
}) })
expectations = Expectations([req], [res])
expectations.patch(monkeypatch)
console.run_command(app, user, 'whoami', []) console.run_command(app, user, 'whoami', [])
mock_get.assert_called_once_with(app, user, '/api/v1/accounts/verify_credentials')
out, err = capsys.readouterr() out, err = capsys.readouterr()
out = uncolorize(out) out = uncolorize(out)
@ -303,52 +265,50 @@ def u(user_id, access_token="abc"):
} }
def test_logout(monkeypatch, capsys): @mock.patch('toot.config.save_config')
def mock_load(): @mock.patch('toot.config.load_config')
return { def test_logout(mock_load, mock_save, capsys):
"users": { mock_load.return_value = {
"king@gizzard.social": u("king@gizzard.social"), "users": {
"lizard@wizard.social": u("lizard@wizard.social"), "king@gizzard.social": u("king@gizzard.social"),
}, "lizard@wizard.social": u("lizard@wizard.social"),
"active_user": "king@gizzard.social", },
} "active_user": "king@gizzard.social",
}
def mock_save(config): console.run_command(app, user, "logout", ["king@gizzard.social"])
assert config["users"] == {
"lizard@wizard.social": u("lizard@wizard.social")
}
assert config["active_user"] is None
monkeypatch.setattr(config, "load_config", mock_load) mock_save.assert_called_once_with({
monkeypatch.setattr(config, "save_config", mock_save) 'users': {
'lizard@wizard.social': u("lizard@wizard.social")
console.run_command(None, None, "logout", ["king@gizzard.social"]) },
'active_user': None
})
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "✓ User king@gizzard.social logged out" in out assert "✓ User king@gizzard.social logged out" in out
def test_activate(monkeypatch, capsys): @mock.patch('toot.config.save_config')
def mock_load(): @mock.patch('toot.config.load_config')
return { def test_activate(mock_load, mock_save, capsys):
"users": { mock_load.return_value = {
"king@gizzard.social": u("king@gizzard.social"), "users": {
"lizard@wizard.social": u("lizard@wizard.social"),
},
"active_user": "king@gizzard.social",
}
def mock_save(config):
assert config["users"] == {
"king@gizzard.social": u("king@gizzard.social"), "king@gizzard.social": u("king@gizzard.social"),
"lizard@wizard.social": u("lizard@wizard.social"), "lizard@wizard.social": u("lizard@wizard.social"),
} },
assert config["active_user"] == "lizard@wizard.social" "active_user": "king@gizzard.social",
}
monkeypatch.setattr(config, "load_config", mock_load) console.run_command(app, user, "activate", ["lizard@wizard.social"])
monkeypatch.setattr(config, "save_config", mock_save)
console.run_command(None, None, "activate", ["lizard@wizard.social"]) mock_save.assert_called_once_with({
'users': {
"king@gizzard.social": u("king@gizzard.social"),
'lizard@wizard.social': u("lizard@wizard.social")
},
'active_user': "lizard@wizard.social"
})
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "✓ User lizard@wizard.social active" in out assert "✓ User lizard@wizard.social active" in out

View File

@ -1,30 +1,6 @@
import requests """
Helpers for testing.
"""
class Expectations():
"""Helper for mocking http requests"""
def __init__(self, requests=[], responses=[]):
self.requests = requests
self.responses = responses
def mock_prepare(self, request):
expected = self.requests.pop(0)
assert request.method == expected.method
assert request.url == expected.url
assert request.data == expected.data
assert request.headers == expected.headers
assert request.params == expected.params
def mock_send(self, *args, **kwargs):
return self.responses.pop(0)
def add(self, req, res):
self.requests.append(req)
self.responses.append(res)
def patch(self, monkeypatch):
monkeypatch.setattr(requests.Session, 'prepare_request', self.mock_prepare)
monkeypatch.setattr(requests.Session, 'send', self.mock_send)
class MockResponse: class MockResponse: