job51 (spiderJobs/platforms/job51/): - client.py: HTTPClient+Job51Sign from crawler_core - api.py: ApiResult→Result, self._http→self.http_client, _request() POST overrides - main.py: BaseFetcher/BaseSearcher from crawler_core - sign.py: backward-compatible stub re-exporting crawler_core.qcwy.sign.Job51Sign zhilian (spiderJobs/platforms/zhilian/): - client.py: HTTPClient+ZhilianSign from crawler_core - api.py: add _parse_zhilian_response (HTTP 200=success), add _parse()/_request() to all classes (GET fetchers + POST searcher overrides) - main.py: BaseFetcher/BaseSearcher from crawler_core - sign.py: backward-compatible stub re-exporting crawler_core.zhilian.sign.ZhilianSign tests: 34 new mock tests (17 job51 + 17 zhilian) Full regression: 98 passed (job51:17 + zhilian:17 + boss:22 + crawler_core:41 + 1)
217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
"""
|
||
前程无忧 (51Job) HTTP 层 mock 测试(ARCH-04 / QUAL-03)
|
||
|
||
使用 unittest.mock.MagicMock 替代真实 HTTP 客户端,无网络依赖。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from unittest.mock import MagicMock
|
||
|
||
from spiderJobs.platforms.job51.api import (
|
||
GetCompanyInfo,
|
||
GetJobDetail,
|
||
SearchCompanyJobs,
|
||
SearchRecommendJobs,
|
||
_parse_job51_response,
|
||
)
|
||
from spiderJobs.platforms.job51.client import Job51Client
|
||
from crawler_core.base import Result
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 1. _parse_job51_response 纯函数测试
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestParseJob51Response:
|
||
|
||
def test_http_error_returns_failure(self):
|
||
result = _parse_job51_response(500, {})
|
||
assert result.success is False
|
||
assert result.status_code == 500
|
||
|
||
def test_status_zero_returns_failure(self):
|
||
result = _parse_job51_response(200, {"status": 0, "message": "系统繁忙"})
|
||
assert result.success is False
|
||
assert "系统繁忙" in result.error
|
||
|
||
def test_status_str_zero_returns_failure(self):
|
||
result = _parse_job51_response(200, {"status": "0", "message": "错误"})
|
||
assert result.success is False
|
||
|
||
def test_status_one_with_resultbody_joblist(self):
|
||
raw = {
|
||
"status": 1,
|
||
"resultbody": {
|
||
"jobList": {"items": [{"jobId": "123", "jobName": "Python 工程师"}], "totalCount": 1}
|
||
}
|
||
}
|
||
result = _parse_job51_response(200, raw)
|
||
assert result.success is True
|
||
assert len(result.list) == 1
|
||
assert result.list[0]["jobName"] == "Python 工程师"
|
||
|
||
def test_status_one_no_items_is_end_page(self):
|
||
raw = {"status": 1, "resultbody": {"jobList": {"items": []}}}
|
||
result = _parse_job51_response(200, raw)
|
||
assert result.success is True
|
||
assert result.is_end_page is True
|
||
|
||
def test_non_dict_raw_returns_failure(self):
|
||
result = _parse_job51_response(200, "not a dict")
|
||
assert result.success is False
|
||
|
||
def test_detail_payload(self):
|
||
raw = {"status": 1, "resultbody": {"companyName": "测试公司"}}
|
||
result = _parse_job51_response(200, raw)
|
||
assert result.success is True
|
||
assert result.data["companyName"] == "测试公司"
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 2. SearchRecommendJobs
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestSearchRecommendJobs:
|
||
|
||
def _make_client(self, return_value):
|
||
mock_client = MagicMock()
|
||
mock_client.post.return_value = return_value
|
||
return mock_client
|
||
|
||
def test_search_success(self):
|
||
raw = {
|
||
"status": 1,
|
||
"resultbody": {
|
||
"jobList": {"items": [{"jobId": "1", "jobName": "测试职位"}]}
|
||
}
|
||
}
|
||
searcher = SearchRecommendJobs(job_area="020000",
|
||
client=self._make_client((200, raw)))
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is True
|
||
assert len(result.list) == 1
|
||
|
||
def test_search_http_error(self):
|
||
searcher = SearchRecommendJobs(client=self._make_client((403, {})))
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is False
|
||
assert result.status_code == 403
|
||
|
||
def test_search_biz_error(self):
|
||
raw = {"status": 0, "message": "接口限流"}
|
||
searcher = SearchRecommendJobs(client=self._make_client((200, raw)))
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is False
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 3. GetJobDetail(路径拼接版)
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestGetJobDetail:
|
||
|
||
def test_fetch_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"status": 1,
|
||
"resultbody": {"jobName": "数据工程师", "salary": "20k-30k"},
|
||
})
|
||
fetcher = GetJobDetail(job_id="170651439", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is True
|
||
assert result.data["jobName"] == "数据工程师"
|
||
|
||
def test_fetch_exception_handled(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.side_effect = ConnectionError("网络超时")
|
||
fetcher = GetJobDetail(job_id="123", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is False
|
||
assert "网络超时" in result.error
|
||
|
||
def test_fetch_http_error(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (404, {})
|
||
fetcher = GetJobDetail(job_id="nonexist", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is False
|
||
assert result.status_code == 404
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 4. GetCompanyInfo
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestGetCompanyInfo:
|
||
|
||
def test_fetch_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"status": 1,
|
||
"resultbody": {"companyName": "测试科技有限公司", "coId": "9825088"},
|
||
})
|
||
fetcher = GetCompanyInfo(company_id="9825088", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is True
|
||
assert result.data["companyName"] == "测试科技有限公司"
|
||
|
||
def test_fetch_exception(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.side_effect = TimeoutError("请求超时")
|
||
fetcher = GetCompanyInfo(company_id="123", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is False
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 5. SearchCompanyJobs
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestSearchCompanyJobs:
|
||
|
||
def test_search_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.post.return_value = (200, {
|
||
"status": 1,
|
||
"resultbody": {"items": [{"jobId": "1"}], "totalCount": 1},
|
||
})
|
||
searcher = SearchCompanyJobs(company_id="9825088", client=mock_client)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is True
|
||
assert len(result.list) == 1
|
||
|
||
def test_search_empty(self):
|
||
mock_client = MagicMock()
|
||
mock_client.post.return_value = (200, {
|
||
"status": 1,
|
||
"resultbody": {"items": [], "totalCount": 0},
|
||
})
|
||
searcher = SearchCompanyJobs(company_id="9825088", client=mock_client)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is True
|
||
assert result.is_end_page is True
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 6. Job51Client — sign 注入
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestJob51ClientHeaders:
|
||
|
||
def test_headers_contain_sign(self):
|
||
client = Job51Client()
|
||
headers = client._job51_headers(sign="test_sign_value")
|
||
assert headers["sign"] == "test_sign_value"
|
||
|
||
def test_headers_uuid_format(self):
|
||
client = Job51Client()
|
||
headers = client._job51_headers(sign="abc")
|
||
assert len(headers["uuid"]) >= 20
|
||
|
||
def test_headers_empty_account(self):
|
||
client = Job51Client()
|
||
headers = client._job51_headers(sign="xyz")
|
||
assert headers["user-token"] == ""
|
||
assert headers["account-id"] == ""
|