From 9f23ba4d55bfa0091aac67913e9e580dac132a32 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Thu, 7 Jun 2018 10:00:50 +0200 Subject: [PATCH] Simplify mocking in tests --- tests/test_api.py | 57 ++++---- tests/test_console.py | 332 +++++++++++++++++++----------------------- tests/utils.py | 30 +--- 3 files changed, 176 insertions(+), 243 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 137c7f8..7bbe8eb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,31 +1,32 @@ # -*- coding: utf-8 -*- import pytest -from requests import Request +from unittest import mock from toot import App, CLIENT_NAME, CLIENT_WEBSITE 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): - request = Request('POST', 'https://bigfish.software/api/v1/apps', - data={'website': CLIENT_WEBSITE, - 'client_name': CLIENT_NAME, - 'scopes': SCOPES, - '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) +@mock.patch('toot.http.anon_post') +def test_create_app(mock_post): + mock_post.return_value = MockResponse({ + 'client_id': 'foo', + 'client_secret': 'bar', + }) 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') data = { @@ -37,23 +38,21 @@ def test_login(monkeypatch): 'scope': SCOPES, } - request = Request('POST', 'https://bigfish.software/oauth/token', data=data) - - response = MockResponse({ + mock_post.return_value = MockResponse({ 'token_type': 'bearer', 'scope': 'read write follow', 'access_token': 'xxx', 'created_at': 1492523699 }) - e = Expectations() - e.add(request, response) - e.patch(monkeypatch) - 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') data = { @@ -65,12 +64,10 @@ def test_login_failed(monkeypatch): 'scope': SCOPES, } - request = Request('POST', 'https://bigfish.software/oauth/token', data=data) - response = MockResponse(is_redirect=True) - - e = Expectations() - e.add(request, response) - e.patch(monkeypatch) + mock_post.return_value = MockResponse(is_redirect=True) with pytest.raises(AuthenticationError): login(app, 'user', 'pass') + + mock_post.assert_called_once_with( + 'https://bigfish.software/oauth/token', data, allow_redirects=False) diff --git a/tests/test_console.py b/tests/test_console.py index c1b3e40..2d3ae85 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- +import io import pytest -import requests 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 tests.utils import MockResponse, Expectations +from tests.utils import MockResponse app = App('habunek.com', 'https://habunek.com', 'foo', 'bar') 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 -def test_post_defaults(monkeypatch, capsys): - def mock_prepare(request): - assert request.method == 'POST' - assert request.url == 'https://habunek.com/api/v1/statuses' - 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) +@mock.patch('toot.http.post') +def test_post_defaults(mock_post, capsys): + mock_post.return_value = MockResponse({ + 'url': 'https://habunek.com/@ihabunek/1234567890' + }) 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() - 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): - def mock_prepare(request): - assert request.method == 'POST' - 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, - } +@mock.patch('toot.http.post') +def test_post_with_options(mock_post, capsys): + args = ['Hello world', '--visibility', 'unlisted'] - 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) - - args = ['"Hello world"', '--visibility', 'unlisted'] + mock_post.return_value = MockResponse({ + 'url': 'https://habunek.com/@ihabunek/1234567890' + }) 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() - 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'] with pytest.raises(SystemExit): @@ -87,7 +77,7 @@ def test_post_invalid_visibility(monkeypatch, capsys): 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'] 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 -def test_timeline(monkeypatch, capsys): - def mock_prepare(request): - assert request.url == 'https://habunek.com/api/v1/timelines/home' - assert request.headers == {'Authorization': 'Bearer xxx'} - assert request.params == {} - - def mock_send(*args, **kwargs): - return MockResponse([{ - 'account': { - 'display_name': 'Frank Zappa', - 'username': 'fz' - }, - 'created_at': '2017-04-12T15:53:18.174Z', - 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", - 'reblog': None, - }]) - - monkeypatch.setattr(requests.Request, 'prepare', mock_prepare) - monkeypatch.setattr(requests.Session, 'send', mock_send) +@mock.patch('toot.http.get') +def test_timeline(mock_get, monkeypatch, capsys): + mock_get.return_value = MockResponse([{ + 'account': { + 'display_name': 'Frank Zappa', + 'username': 'fz' + }, + 'created_at': '2017-04-12T15:53:18.174Z', + 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", + 'reblog': None, + }]) console.run_command(app, user, 'timeline', []) + mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home') + out, err = capsys.readouterr() assert "The computer can't tell you the emotional story." in out assert "Frank Zappa @fz" in out -def test_upload(monkeypatch, capsys): - def mock_prepare(request): - assert request.method == 'POST' - assert request.url == 'https://habunek.com/api/v1/media' - assert request.headers == {'Authorization': 'Bearer xxx'} - assert request.files.get('file') is not None - - def mock_send(*args, **kwargs): - 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) +@mock.patch('toot.http.post') +def test_upload(mock_post, capsys): + mock_post.return_value = 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', + }) 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() assert "Uploading media" in out assert __file__ in out -def test_search(monkeypatch, capsys): - def mock_prepare(request): - assert request.url == 'https://habunek.com/api/v1/search' - assert request.headers == {'Authorization': 'Bearer xxx'} - assert request.params == { - 'q': 'freddy', - 'resolve': False, - } - - def mock_send(*args, **kwargs): - return MockResponse({ - 'hashtags': ['foo', 'bar', 'baz'], - '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) +@mock.patch('toot.http.get') +def test_search(mock_get, capsys): + mock_get.return_value = MockResponse({ + 'hashtags': ['foo', 'bar', 'baz'], + 'accounts': [{ + 'acct': 'thequeen', + 'display_name': 'Freddy Mercury' + }, { + 'acct': 'thequeen@other.instance', + 'display_name': 'Mercury Freddy' + }], + 'statuses': [], + }) 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() assert "Hashtags:\n\033[32m#foo\033[0m, \033[32m#bar\033[0m, \033[32m#baz\033[0m" 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 -def test_follow(monkeypatch, capsys): - req1 = Request('GET', 'https://habunek.com/api/v1/accounts/search', - params={'q': 'blixa'}, - headers={'Authorization': 'Bearer xxx'}) - res1 = MockResponse([ +@mock.patch('toot.http.post') +@mock.patch('toot.http.get') +def test_follow(mock_get, mock_post, capsys): + mock_get.return_value = MockResponse([ {'id': 123, 'acct': 'blixa@other.acc'}, {'id': 321, 'acct': 'blixa'}, ]) - - 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) + mock_post.return_value = MockResponse() 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() assert "You are now following blixa" in out -def test_follow_not_found(monkeypatch, capsys): - req = Request('GET', 'https://habunek.com/api/v1/accounts/search', - params={'q': 'blixa'}, headers={'Authorization': 'Bearer xxx'}) - res = MockResponse() - - expectations = Expectations([req], [res]) - expectations.patch(monkeypatch) +@mock.patch('toot.http.get') +def test_follow_not_found(mock_get, capsys): + mock_get.return_value = MockResponse() with pytest.raises(ConsoleError) as ex: 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) -def test_unfollow(monkeypatch, capsys): - req1 = Request('GET', 'https://habunek.com/api/v1/accounts/search', - params={'q': 'blixa'}, - headers={'Authorization': 'Bearer xxx'}) - res1 = MockResponse([ +@mock.patch('toot.http.post') +@mock.patch('toot.http.get') +def test_unfollow(mock_get, mock_post, capsys): + mock_get.return_value = MockResponse([ {'id': 123, 'acct': 'blixa@other.acc'}, {'id': 321, 'acct': 'blixa'}, ]) - req2 = Request('POST', 'https://habunek.com/api/v1/accounts/321/unfollow', - headers={'Authorization': 'Bearer xxx'}) - res2 = MockResponse() - - expectations = Expectations([req1, req2], [res1, res2]) - expectations.patch(monkeypatch) + mock_post.return_value = MockResponse() 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() assert "You are no longer following blixa" in out -def test_unfollow_not_found(monkeypatch, capsys): - req = Request('GET', 'https://habunek.com/api/v1/accounts/search', - params={'q': 'blixa'}, headers={'Authorization': 'Bearer xxx'}) - res = MockResponse([]) - - expectations = Expectations([req], [res]) - expectations.patch(monkeypatch) +@mock.patch('toot.http.get') +def test_unfollow_not_found(mock_get, capsys): + mock_get.return_value = MockResponse([]) with pytest.raises(ConsoleError) as ex: 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) -def test_whoami(monkeypatch, capsys): - req = Request('GET', 'https://habunek.com/api/v1/accounts/verify_credentials', - headers={'Authorization': 'Bearer xxx'}) - - res = MockResponse({ +@mock.patch('toot.http.get') +def test_whoami(mock_get, capsys): + mock_get.return_value = MockResponse({ 'acct': 'ihabunek', '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', @@ -276,11 +239,10 @@ def test_whoami(monkeypatch, capsys): 'username': 'ihabunek' }) - expectations = Expectations([req], [res]) - expectations.patch(monkeypatch) - console.run_command(app, user, 'whoami', []) + mock_get.assert_called_once_with(app, user, '/api/v1/accounts/verify_credentials') + out, err = capsys.readouterr() out = uncolorize(out) @@ -303,52 +265,50 @@ def u(user_id, access_token="abc"): } -def test_logout(monkeypatch, capsys): - def mock_load(): - return { - "users": { - "king@gizzard.social": u("king@gizzard.social"), - "lizard@wizard.social": u("lizard@wizard.social"), - }, - "active_user": "king@gizzard.social", - } +@mock.patch('toot.config.save_config') +@mock.patch('toot.config.load_config') +def test_logout(mock_load, mock_save, capsys): + mock_load.return_value = { + "users": { + "king@gizzard.social": u("king@gizzard.social"), + "lizard@wizard.social": u("lizard@wizard.social"), + }, + "active_user": "king@gizzard.social", + } - def mock_save(config): - assert config["users"] == { - "lizard@wizard.social": u("lizard@wizard.social") - } - assert config["active_user"] is None + console.run_command(app, user, "logout", ["king@gizzard.social"]) - monkeypatch.setattr(config, "load_config", mock_load) - monkeypatch.setattr(config, "save_config", mock_save) - - console.run_command(None, None, "logout", ["king@gizzard.social"]) + mock_save.assert_called_once_with({ + 'users': { + 'lizard@wizard.social': u("lizard@wizard.social") + }, + 'active_user': None + }) out, err = capsys.readouterr() assert "✓ User king@gizzard.social logged out" in out -def test_activate(monkeypatch, capsys): - def mock_load(): - return { - "users": { - "king@gizzard.social": u("king@gizzard.social"), - "lizard@wizard.social": u("lizard@wizard.social"), - }, - "active_user": "king@gizzard.social", - } - - def mock_save(config): - assert config["users"] == { +@mock.patch('toot.config.save_config') +@mock.patch('toot.config.load_config') +def test_activate(mock_load, mock_save, capsys): + mock_load.return_value = { + "users": { "king@gizzard.social": u("king@gizzard.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) - monkeypatch.setattr(config, "save_config", mock_save) + console.run_command(app, user, "activate", ["lizard@wizard.social"]) - 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() assert "✓ User lizard@wizard.social active" in out diff --git a/tests/utils.py b/tests/utils.py index d8242b5..cdae09c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,30 +1,6 @@ -import requests - - -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) +""" +Helpers for testing. +""" class MockResponse: