Skip to content

Commit

Permalink
Unit tests using Pytest for github_projects.py. (#58)
Browse files Browse the repository at this point in the history
* Unit tests using Pytest for github_projects.py.
  • Loading branch information
MobiTikula authored Oct 30, 2024
1 parent c0ae5ee commit f7f8814
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 10 deletions.
17 changes: 8 additions & 9 deletions living_documentation_generator/github_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __initialize_request_session(self) -> requests.Session:

return self.__session

def __send_graphql_query(self, query: str) -> Optional[dict[str, dict]]:
def _send_graphql_query(self, query: str) -> Optional[dict[str, dict]]:
"""
Send a GraphQL query to the GitHub API and returns the response.
If an HTTP error occurs, it prints the error and returns None instead.
Expand Down Expand Up @@ -105,7 +105,7 @@ def get_repository_projects(self, repository: Repository, projects_title_filter:
organization_name=repository.owner.login, repository_name=repository.name
)

projects_from_repo_response = self.__send_graphql_query(projects_from_repo_query)
projects_from_repo_response = self._send_graphql_query(projects_from_repo_query)

if projects_from_repo_response is None:
logger.warning(
Expand All @@ -129,9 +129,6 @@ def get_repository_projects(self, repository: Repository, projects_title_filter:
# If no filter is provided, all projects are required
is_project_required = True if not projects_title_filter else project_title in projects_title_filter

if not is_project_required:
logger.debug("Project `%s` is not required based on the filter.", project_title)

# Main project structure is loaded and added to the projects list
if is_project_required:
# Fetch the project field options from the GraphQL API
Expand All @@ -140,12 +137,14 @@ def get_repository_projects(self, repository: Repository, projects_title_filter:
repository_name=repository.name,
project_number=project_number,
)
field_option_response = self.__send_graphql_query(project_field_options_query)
field_option_response = self._send_graphql_query(project_field_options_query)

# Create the GitHub project instance and add it to the output list
project = GithubProject().loads(project_json, repository, field_option_response)
if project not in projects:
projects.append(project)
else:
logger.debug("Project `%s` is not required based on the filter.", project_title)

else:
logger.warning("Repository information is not present in the response")
Expand Down Expand Up @@ -173,7 +172,7 @@ def get_project_issues(self, project: GithubProject) -> list[ProjectIssue]:
project_id=project.id, after_argument=after_argument
)

project_issues_response = self.__send_graphql_query(issues_from_project_query)
project_issues_response = self._send_graphql_query(issues_from_project_query)

# Return empty list, if project has no issues attached
if not project_issues_response:
Expand All @@ -185,7 +184,7 @@ def get_project_issues(self, project: GithubProject) -> list[ProjectIssue]:

# Extend project issues list per every page during pagination
project_issues_raw.extend(project_issue_data)
logger.debug("Received `%s` issue records from project: %s.", len(project_issue_data), project.title)
logger.debug("Received `%i` issue(s) records from project: %s.", len(project_issue_data), project.title)

# Check for closing the pagination process
if not page_info["hasNextPage"]:
Expand All @@ -197,6 +196,6 @@ def get_project_issues(self, project: GithubProject) -> list[ProjectIssue]:
for issue in (ProjectIssue().loads(issue_json, project) for issue_json in project_issues_raw)
if issue is not None
]
logger.debug("Loaded `%s` issues from project: %s.", len(project_issues), project.title)
logger.debug("Loaded `%i` issue(s) from project: %s.", len(project_issues), project.title)

return project_issues
11 changes: 10 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from github.Rate import Rate
from github.RateLimit import RateLimit
from github.Repository import Repository
from pytest_mock import mocker

from living_documentation_generator.model.github_project import GithubProject
from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter
Expand Down Expand Up @@ -66,3 +65,13 @@ def github_project_setup(mocker):
project.loads(project_json, repository, field_option_response)

return project


@pytest.fixture
def repository_setup(mocker):
repository = mocker.Mock(spec=Repository)
repository.owner.login = "test_owner"
repository.name = "test_repo"
repository.full_name = "test_owner/test_repo"

return repository
245 changes: 245 additions & 0 deletions tests/test_github_projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
#
# Copyright 2024 ABSA Group Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import requests

from living_documentation_generator.github_projects import GithubProjects


# _send_graphql_query


def test_send_graphql_query_correct_behaviour(mocker):
expected_data = {"repository": {"projectsV2": {"nodes": [{"id": "PVT_k", "number": 3, "title": "Board"}]}}}
expected_response = {"data": expected_data}

mock_session = mocker.Mock()
mock_session.post.return_value.json.return_value = expected_response
mock_session.post.return_value.raise_for_status = lambda: None

mocker.patch.object(
GithubProjects,
"_GithubProjects__initialize_request_session",
lambda self: setattr(self, "_GithubProjects__session", mock_session),
)

actual_data = GithubProjects("token123")._send_graphql_query("query")

assert expected_data == actual_data


def test_send_graphql_query_http_error(mocker):
mock_session = mocker.Mock()
mocker.patch.object(
GithubProjects,
"_GithubProjects__initialize_request_session",
lambda self: setattr(self, "_GithubProjects__session", mock_session),
)
mock_log_error = mocker.patch("living_documentation_generator.github_projects.logger.error")
mock_session.post.side_effect = requests.HTTPError("HTTP error occurred")

actual = GithubProjects("token123")._send_graphql_query("query")

assert actual is None
mock_log_error.assert_called_once_with("HTTP error occurred: %s.", mocker.ANY, exc_info=True)


def test_send_graphql_query_request_exception(mocker):
mock_session = mocker.Mock()
mocker.patch.object(
GithubProjects,
"_GithubProjects__initialize_request_session",
lambda self: setattr(self, "_GithubProjects__session", mock_session),
)
mock_log_error = mocker.patch("living_documentation_generator.github_projects.logger.error")
mock_session.post.side_effect = requests.RequestException("An error occurred.")

actual = GithubProjects("token123")._send_graphql_query("query")

assert actual is None
mock_log_error.assert_called_once_with("An error occurred: %s.", mocker.ANY, exc_info=True)


# get_repository_projects


def test_get_repository_projects_correct_behaviour(mocker, repository_setup):
# Arrange
mock_repository = repository_setup()
projects_title_filter = ["Project A"]
mock_logger_debug = mocker.patch("living_documentation_generator.github_projects.logger.debug")

mocker.patch(
"living_documentation_generator.github_projects.get_projects_from_repo_query",
return_value="mocked_projects_query",
)
mocker.patch(
"living_documentation_generator.github_projects.get_project_field_options_query",
return_value="mocked_project_field_options_query",
)
mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query")
mock_send_query.side_effect = [
{
"repository": {
"projectsV2": {"nodes": [{"title": "Project A", "number": 1}, {"title": "Project B", "number": 2}]}
}
},
{"data": {}},
]

mock_github_project = mocker.patch("living_documentation_generator.github_projects.GithubProject")
mock_github_project_instance = mock_github_project.return_value
mock_github_project_instance.loads.return_value = mock_github_project_instance

# Act
github_projects_instance = GithubProjects("token123")
actual = github_projects_instance.get_repository_projects(mock_repository, projects_title_filter)

# Assert
assert 1 == len(actual)
assert mock_github_project_instance in actual
mock_send_query.assert_called()
mock_github_project_instance.loads.assert_called_once_with(
{"title": "Project A", "number": 1}, mock_repository, {"data": {}}
)
mock_logger_debug.assert_called_once_with("Project `%s` is not required based on the filter.", "Project B")


def test_get_repository_projects_response_none(mocker, repository_setup):
# Arrange
mock_repository = repository_setup()
mock_logger_warning = mocker.patch("living_documentation_generator.github_projects.logger.warning")

mocker.patch(
"living_documentation_generator.github_projects.get_projects_from_repo_query",
return_value="mocked_projects_query",
)
mocker.patch(
"living_documentation_generator.github_projects.get_project_field_options_query",
return_value="mocked_project_field_options_query",
)
mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query", return_value=None)

# Act
github_projects_instance = GithubProjects("mock_token")
actual = github_projects_instance.get_repository_projects(mock_repository, [])

# Assert
assert [] == actual
mock_send_query.assert_called_once_with("mocked_projects_query")
mock_logger_warning.assert_called_once_with(
"Fetching GitHub project data - no project data for repository %s. No data received.", mock_repository.full_name
)


def test_get_repository_projects_response_nodes_none(mocker, repository_setup):
# Arrange
mock_repository = repository_setup()
mock_logger_warning = mocker.patch("living_documentation_generator.github_projects.logger.warning")

mocker.patch(
"living_documentation_generator.github_projects.get_projects_from_repo_query",
return_value="mocked_projects_query",
)
mocker.patch(
"living_documentation_generator.github_projects.get_project_field_options_query",
return_value="mocked_project_field_options_query",
)
mock_send_query = mocker.patch.object(
GithubProjects, "_send_graphql_query", return_value={"repository": {"projectsV2": {"nodes": None}}}
)

# Act
github_projects_instance = GithubProjects("mock_token")
actual = github_projects_instance.get_repository_projects(mock_repository, [])

# Assert
assert actual == []
mock_send_query.assert_called_once_with("mocked_projects_query")
mock_logger_warning.assert_called_once_with("Repository information is not present in the response")


# get_project_issues


def test_get_project_issues_correct_behaviour(mocker, github_project_setup):
mock_project = github_project_setup()
mock_logger_debug = mocker.patch("living_documentation_generator.github_projects.logger.debug")

mocker.patch(
"living_documentation_generator.github_projects.get_issues_from_project_query",
return_value="mocked_issues_query",
)

mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query")
mock_send_query.side_effect = [
{
"node": {
"items": {
"nodes": [{"id": "issue_1", "title": "Issue 1"}, {"id": "issue_2", "title": "Issue 2"}],
"pageInfo": {"hasNextPage": True, "endCursor": "cursor_1"},
}
}
},
{
"node": {
"items": {
"nodes": [{"id": "issue_3", "title": "Issue 3"}, {"id": "issue_4", "title": "Issue 4"}],
"pageInfo": {"hasNextPage": False, "endCursor": "cursor_2"},
}
}
},
]

mock_project_issue = mocker.patch("living_documentation_generator.github_projects.ProjectIssue")
mock_project_issue_instance = mock_project_issue.return_value
mock_project_issue_instance.loads.side_effect = lambda issue_data, proj: issue_data if issue_data else None

# Act
github_projects_instance = GithubProjects("token123")
actual = github_projects_instance.get_project_issues(mock_project)

# Assert
assert 4 == len(actual)
assert actual == [
{"id": "issue_1", "title": "Issue 1"},
{"id": "issue_2", "title": "Issue 2"},
{"id": "issue_3", "title": "Issue 3"},
{"id": "issue_4", "title": "Issue 4"},
]
mock_send_query.assert_called()
mock_project_issue_instance.loads.assert_any_call({"id": "issue_1", "title": "Issue 1"}, mock_project)
mock_project_issue_instance.loads.assert_any_call({"id": "issue_4", "title": "Issue 4"}, mock_project)
mock_logger_debug.assert_any_call("Received `%i` issue(s) records from project: %s.", 2, mock_project.title)
mock_logger_debug.assert_any_call("Loaded `%i` issue(s) from project: %s.", 4, mock_project.title)


def test_get_project_issues_no_response(mocker, github_project_setup):
# Arrange
mock_project = github_project_setup()

mocker.patch(
"living_documentation_generator.github_projects.get_issues_from_project_query",
return_value="mocked_issues_query",
)
mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query", return_value=None)

# Act
github_projects_instance = GithubProjects("mock_token")
result = github_projects_instance.get_project_issues(mock_project)

# Assert
assert result == []
mock_send_query.assert_called_once_with("mocked_issues_query")

0 comments on commit f7f8814

Please sign in to comment.