Repository: relekang/python-semantic-release
Pull request #234 (2020-05-11 15:02:21)

Capture logging output in tests
Adapt the CLI tests so that pytest can capture the log output, and display it if there is a failure. This helps with debugging.

relekang: I think we loose some value by not running the click parser in the tests. 🤔 
AlphaMycelium: @relekang I kept the click parser in tests such as `test_force_major`, which are only checking that CLI arguments translate into the correct function call. We can always create more of those to test the parsing separately from other logic.

Patch:
diff --git a/semantic_release/cli.py b/semantic_release/cli.py
index 3ee35bf..f3a25c9 100644
--- a/semantic_release/cli.py
+++ b/semantic_release/cli.py
@@ -39,7 +39,6 @@ from .vcs_helpers import (
 )
 
 logger = logging.getLogger("semantic_release")
-click_log.basic_config(logger)
 
 SECRET_NAMES = [
     "PYPI_USERNAME",
@@ -85,13 +84,12 @@ def common_options(func):
     return func
 
 
-def version(**kwargs):
+def version(*, retry=False, noop=False, force_level=None, **kwargs):
     """
     Detect the new version according to git log and semver.
 
     Write the new version number and commit it, unless the noop option is True.
     """
-    retry = kwargs.get("retry")
     if retry:
         logger.info("Retrying publication of the same version")
     else:
@@ -105,14 +103,14 @@ def version(**kwargs):
         logger.error(str(e))
         return False
     # Find what the new version number should be
-    level_bump = evaluate_version_bump(current_version, kwargs["force_level"])
+    level_bump = evaluate_version_bump(current_version, force_level)
     new_version = get_new_version(current_version, level_bump)
 
     if new_version == current_version and not retry:
         logger.info("No release will be made.")
         return False
 
-    if kwargs["noop"] is True:
+    if noop:
         logger.warning(
             "No operation mode. Should have bumped "
             f"from {current_version} to {new_version}"
@@ -145,7 +143,7 @@ def version(**kwargs):
     return True
 
 
-def changelog(**kwargs):
+def changelog(*, unreleased=False, noop=False, post=False, **kwargs):
     """
     Generate the changelog since the last release.
 
@@ -162,14 +160,14 @@ def changelog(**kwargs):
     previous_version = get_previous_version(current_version)
 
     # Generate the changelog
-    if kwargs["unreleased"]:
+    if unreleased:
         log = generate_changelog(current_version, None)
     else:
         log = generate_changelog(previous_version, current_version)
     logger.info(markdown_changelog(current_version, log, header=False))
 
     # Post changelog to HVCS if enabled
-    if not kwargs.get("noop") and kwargs.get("post"):
+    if not noop and post:
         if check_token():
             owner, name = get_repository_owner_and_name()
             logger.info("Posting changelog to HVCS")
@@ -196,7 +194,7 @@ def publish(**kwargs):
         current_version = get_previous_version(current_version)
     else:
         # Calculate the new version
-        level_bump = evaluate_version_bump(current_version, kwargs["force_level"])
+        level_bump = evaluate_version_bump(current_version, kwargs.get("force_level"))
         new_version = get_new_version(current_version, level_bump)
 
     owner, name = get_repository_owner_and_name()
@@ -282,6 +280,8 @@ def filter_output_for_secrets(message):
 
 
 def entry():
+    click_log.basic_config(logger)
+
     # Move flags to after the command
     ARGS = sorted(sys.argv[1:], key=lambda x: 1 if x.startswith("--") else -1)
     main(args=ARGS)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ce18e3d..b039b4e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -24,7 +24,7 @@ def test_main_should_call_correct_function(mocker, runner):
     assert result.exit_code == 0
 
 
-def test_version_by_commit_should_call_correct_functions(mocker, runner):
+def test_version_by_commit_should_call_correct_functions(mocker):
     def wrapped_config_getboolean(*args, **kwargs):
         if (
             len(args) >= 2
@@ -49,18 +49,18 @@ def test_version_by_commit_should_call_correct_functions(mocker, runner):
         "semantic_release.cli.get_current_version", return_value="1.2.3"
     )
 
-    result = runner.invoke(main, ["version"])
+    version()
+
     mock_current_version.assert_called_once_with()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
     mock_new_version.assert_called_once_with("1.2.3", "major")
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_commit_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
-    assert result.exit_code == 0
 
 
 def test_version_by_tag_with_commit_version_number_should_call_correct_functions(
-    mocker, runner
+    mocker
 ):
     orig = semantic_release.cli.config.get
 
@@ -104,17 +104,18 @@ def test_version_by_tag_with_commit_version_number_should_call_correct_functions
     mock_current_version = mocker.patch(
         "semantic_release.cli.get_current_version", return_value="1.2.3"
     )
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     mock_current_version.assert_called_once_with()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
     mock_new_version.assert_called_once_with("1.2.3", "major")
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_commit_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
-    assert result.exit_code == 0
 
 
-def test_version_by_tag_should_call_correct_functions(mocker, runner):
+def test_version_by_tag_should_call_correct_functions(mocker):
     orig = semantic_release.cli.config.get
 
     def wrapped_config_get(*args, **kwargs):
@@ -139,13 +140,14 @@ def test_version_by_tag_should_call_correct_functions(mocker, runner):
     mock_current_version = mocker.patch(
         "semantic_release.cli.get_current_version", return_value="1.2.3"
     )
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     mock_current_version.assert_called_once_with()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
     mock_new_version.assert_called_once_with("1.2.3", "major")
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
-    assert result.exit_code == 0
 
 
 def test_force_major(mocker, runner):
@@ -187,16 +189,17 @@ def test_retry(mocker, runner):
     assert result.exit_code == 0
 
 
-def test_noop_mode(mocker, runner):
+def test_noop_mode(mocker):
     mock_tag_new_version = mocker.patch("semantic_release.cli.tag_new_version")
     mock_set_new = mocker.patch("semantic_release.cli.commit_new_version")
     mock_commit_new = mocker.patch("semantic_release.cli.set_new_version")
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "major")
-    result = runner.invoke(main, ["version", "--noop"])
+
+    version(noop=True)
+
     assert not mock_set_new.called
     assert not mock_commit_new.called
     assert not mock_tag_new_version.called
-    assert result.exit_code == 0
 
 
 def test_version_no_change(mocker, runner):
@@ -212,17 +215,18 @@ def test_version_no_change(mocker, runner):
     mock_current_version = mocker.patch(
         "semantic_release.cli.get_current_version", return_value="1.2.3"
     )
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     mock_current_version.assert_called_once_with()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
     mock_new_version.assert_called_once_with("1.2.3", None)
     assert not mock_set_new_version.called
     assert not mock_commit_new_version.called
     assert not mock_tag_new_version.called
-    assert result.exit_code == 0
 
 
-def test_version_check_build_status_fails(mocker, runner):
+def test_version_check_build_status_fails(mocker):
     mock_check_build_status = mocker.patch(
         "semantic_release.cli.check_build_status", return_value=False
     )
@@ -231,15 +235,16 @@ def test_version_check_build_status_fails(mocker, runner):
     mock_set_new = mocker.patch("semantic_release.cli.set_new_version")
     mocker.patch("semantic_release.cli.config.getboolean", lambda *x: True)
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "major")
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     assert mock_check_build_status.called
     assert not mock_set_new.called
     assert not mock_commit_new.called
     assert not mock_tag_new_version.called
-    assert result.exit_code == 0
 
 
-def test_version_by_commit_check_build_status_succeeds(mocker, runner):
+def test_version_by_commit_check_build_status_succeeds(mocker):
     mocker.patch("semantic_release.cli.config.getboolean", lambda *x, **y: True)
     mock_check_build_status = mocker.patch(
         "semantic_release.cli.check_build_status", return_value=True
@@ -248,15 +253,16 @@ def test_version_by_commit_check_build_status_succeeds(mocker, runner):
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "major")
     mock_commit_new = mocker.patch("semantic_release.cli.commit_new_version")
     mock_set_new = mocker.patch("semantic_release.cli.set_new_version")
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     assert mock_check_build_status.called
     assert mock_set_new.called
     assert mock_commit_new.called
     assert mock_tag_new_version.called
-    assert result.exit_code == 0
 
 
-def test_version_by_tag_check_build_status_succeeds(mocker, runner):
+def test_version_by_tag_check_build_status_succeeds(mocker):
     orig_get = semantic_release.cli.config.get
 
     def wrapped_config_get(*args, **kwargs):
@@ -287,21 +293,24 @@ def test_version_by_tag_check_build_status_succeeds(mocker, runner):
     mock_set_new_version = mocker.patch("semantic_release.cli.set_new_version")
     mock_tag_new_version = mocker.patch("semantic_release.cli.tag_new_version")
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "major")
-    result = runner.invoke(main, ["version"])
+
+    version()
+
     assert mock_check_build_status.called
     assert mock_set_new_version.called
     assert mock_tag_new_version.called
-    assert result.exit_code == 0
 
 
-def test_version_check_build_status_not_called_if_disabled(mocker, runner):
+def test_version_check_build_status_not_called_if_disabled(mocker):
     mock_check_build_status = mocker.patch("semantic_release.cli.check_build_status")
-    mocker.patch("semantic_release.cli.config.getboolean", lambda *x: False)
-    mocker.patch("semantic_release.cli.tag_new_version", None)
+    mocker.patch("semantic_release.cli.config.getboolean", lambda *x, **y: False)
+    mocker.patch("semantic_release.cli.tag_new_version")
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "major")
-    mocker.patch("semantic_release.cli.commit_new_version", None)
-    mocker.patch("semantic_release.cli.set_new_version", None)
-    runner.invoke(main, ["version"])
+    mocker.patch("semantic_release.cli.commit_new_version")
+    mocker.patch("semantic_release.cli.set_new_version")
+
+    version()
+
     assert not mock_check_build_status.called
 
 
@@ -335,24 +344,26 @@ def test_version_retry(mocker):
     mock_get_new.assert_called_once_with("current", "patch")
 
 
-def test_publish_should_not_upload_to_pypi_if_option_is_false(mocker, runner):
+def test_publish_should_not_upload_to_pypi_if_option_is_false(mocker):
     mocker.patch("semantic_release.cli.checkout")
     mocker.patch("semantic_release.cli.ci_checks.check")
     mock_upload_pypi = mocker.patch("semantic_release.cli.upload_to_pypi")
     mock_upload_release = mocker.patch("semantic_release.cli.upload_to_release")
     mocker.patch("semantic_release.cli.post_changelog", lambda *x: True)
-    mocker.patch("semantic_release.cli.push_new_version", lambda *x: True)
+    mocker.patch("semantic_release.cli.push_new_version", return_value=True)
     mocker.patch("semantic_release.cli.version", lambda: True)
     mocker.patch("semantic_release.cli.markdown_changelog", lambda *x, **y: "CHANGES")
     mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
     mocker.patch("semantic_release.cli.check_token", lambda: True)
     mocker.patch("semantic_release.cli.config.getboolean", lambda *x: False)
-    runner.invoke(main, ["publish"])
+
+    publish()
+
     assert not mock_upload_pypi.called
     assert not mock_upload_release.called
 
 
-def test_publish_should_do_nothing_when_version_fails(mocker, runner):
+def test_publish_should_do_nothing_when_version_fails(mocker):
     mocker.patch("semantic_release.cli.checkout")
     mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
     mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "feature")
@@ -363,19 +374,18 @@ def test_publish_should_do_nothing_when_version_fails(mocker, runner):
     mock_push = mocker.patch("semantic_release.cli.push_new_version")
     mock_ci_check = mocker.patch("semantic_release.ci_checks.check")
     mock_version = mocker.patch("semantic_release.cli.version", return_value=False)
-    result = runner.invoke(main, ["publish"])
-    mock_version.assert_called_once_with(
-        noop=False, post=False, force_level=None, retry=False, define=(),
-    )
+
+    publish()
+
+    assert mock_version.called
     assert not mock_push.called
     assert not mock_upload_pypi.called
     assert not mock_upload_release.called
     assert not mock_log.called
     assert mock_ci_check.called
-    assert result.exit_code == 0
 
 
-def test_publish_should_call_functions(mocker, runner):
+def test_publish_should_call_functions(mocker):
     mock_push = mocker.patch("semantic_release.cli.push_new_version")
     mock_checkout = mocker.patch("semantic_release.cli.checkout")
     mock_version = mocker.patch("semantic_release.cli.version", return_value=True)
@@ -395,19 +405,15 @@ def test_publish_should_call_functions(mocker, runner):
     mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
     mocker.patch("semantic_release.cli.check_token", lambda: True)
 
-    result = runner.invoke(main, ["publish"])
-    print(result.output)  # Print output for debugging should the test fail
+    publish()
 
-    assert result.exit_code == 0
     assert mock_ci_check.called
     assert mock_push.called
     assert mock_remove_dists.called
     assert mock_build_dists.called
     assert mock_pypi.called
     assert mock_release.called
-    mock_version.assert_called_once_with(
-        noop=False, post=False, force_level=None, retry=False, define=(),
-    )
+    assert mock_version.called
     mock_log.assert_called_once_with(
         u"relekang", "python-semantic-release", "2.0.0", "CHANGES"
     )