-
-
Notifications
You must be signed in to change notification settings - Fork 109
[WIP] News specの強化(追加ケースの実装) #1719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- news:fetchタスクでfileスキーム/ローカルパスをsafe_openで許可(テスト用RSSの読み込みに対応) - YAML書き込み・ログ出力のパスをENV['NEWS_YAML_PATH']で上書き可能なyaml_pathに統一 - ソート後に全アイテムへ1からの連番IDを再付与 - news:import_from_yamlタスクもENV['NEWS_YAML_PATH']に対応
@claude 想定するケースは以下の3つです。 1,ネットワークエラー(safe_open が例外を投げる) これらを網羅的にテストするための、 |
Claude finished @nacchan99's task —— View job タスクリスト
完成したエラーハンドリングテスト
テストコードの全文require 'rails_helper'
require 'rake'
require 'yaml'
require 'net/http'
RSpec.describe 'news:fetch エラーハンドリング', type: :task do
before { Rails.application.load_tasks }
before { allow(Rails.env).to receive(:test?).and_return(true) }
let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') }
let(:fetch_task) { Rake::Task['news:fetch'] }
let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) }
before do
ENV['NEWS_YAML_PATH'] = yaml_path.to_s
fetch_task.reenable
# ロガーのモック設定
allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock)
allow(logger_mock).to receive(:info)
allow(logger_mock).to receive(:warn)
end
after do
ENV.delete('NEWS_YAML_PATH')
ENV.delete('NEWS_RSS_PATH')
File.delete(yaml_path) if File.exist?(yaml_path)
end
describe 'ネットワークエラーのハンドリング' do
context 'safe_open がネットワークエラーで例外を投げる場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト')
end
it 'エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 接続タイムアウト/)
expect { fetch_task.invoke }.not_to raise_error
# 空の news.yml が作成される
expect(File.exist?(yaml_path)).to be true
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to eq([])
end
end
context 'HTTPエラーレスポンスの場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::HTTPServerException, '500 Internal Server Error')
end
it 'エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 500 Internal Server Error/)
expect { fetch_task.invoke }.not_to raise_error
end
end
context '不正なURLの場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
allow_any_instance_of(Object).to receive(:safe_open).and_raise('不正なURLです: https://example.com/feed.rss')
end
it 'エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 不正なURLです/)
expect { fetch_task.invoke }.not_to raise_error
end
end
end
describe '不正なRSSのハンドリング' do
context 'RSS::Parser.parse が失敗する場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
# safe_open は成功するが、不正なXMLを返す
allow_any_instance_of(Object).to receive(:safe_open).and_return('<invalid>not valid rss</invalid>')
end
it 'エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: /)
expect { fetch_task.invoke }.not_to raise_error
# 空の news.yml が作成される
expect(File.exist?(yaml_path)).to be true
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to eq([])
end
end
context '空のRSSフィードの場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
# 有効だが空のRSSフィード
empty_rss = <<~RSS
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Empty Feed</title>
<description>Empty RSS Feed</description>
<link>https://example.com</link>
</channel>
</rss>
RSS
allow_any_instance_of(Object).to receive(:safe_open).and_return(empty_rss)
end
it '空の配列として処理し、エラーにならない' do
expect { fetch_task.invoke }.not_to raise_error
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to eq([])
end
end
context 'RSSアイテムに必須フィールドが欠けている場合' do
before do
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
# linkやpubDateが欠けているRSS
invalid_rss = <<~RSS
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Invalid Feed</title>
<description>Invalid RSS Feed</description>
<link>https://example.com</link>
<item>
<title>タイトルのみの記事</title>
<!-- link と pubDate が欠けている -->
</item>
</channel>
</rss>
RSS
allow_any_instance_of(Object).to receive(:safe_open).and_return(invalid_rss)
end
it 'エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/)
expect { fetch_task.invoke }.not_to raise_error
end
end
end
describe '破損したYAMLファイルのハンドリング' do
context '既存のYAMLファイルが破損している場合' do
before do
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
# 破損したYAMLファイルを作成
File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here")
end
it 'YAML読み込みエラーが発生し、タスクが失敗する' do
# YAML.safe_load のエラーは rescue されないため、タスク全体が失敗する
expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError)
end
end
context '既存のYAMLファイルが不正な構造の場合' do
before do
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
# 不正な構造のYAMLファイル(newsキーがない)
File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml)
end
it '空の配列として扱い、処理を継続する' do
expect { fetch_task.invoke }.not_to raise_error
# 新しいデータで上書きされる
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to be_an(Array)
expect(yaml_content['news'].size).to be > 0
end
end
context '許可されていないクラスを含むYAMLファイルの場合' do
before do
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
# DateTimeオブジェクトを含むYAML(Timeのみ許可されている)
yaml_content = {
'news' => [
{
'id' => 1,
'url' => 'https://example.com/test',
'title' => 'テスト',
'published_at' => DateTime.now
}
]
}
# 強制的にDateTimeオブジェクトを含むYAMLを作成
File.write(yaml_path, yaml_content.to_yaml.gsub('!ruby/object:DateTime', '!ruby/object:DateTime'))
end
it 'YAML読み込みエラーが発生し、タスクが失敗する' do
expect { fetch_task.invoke }.to raise_error(Psych::DisallowedClass)
end
end
end
describe '複数のエラーが同時に発生する場合' do
context '複数のRSSフィードで異なるエラーが発生する場合' do
before do
# 複数のフィードURLを環境変数経由では設定できないため、
# デフォルトの動作をオーバーライドする
allow(Rails.env).to receive(:test?).and_return(false)
allow(Rails.env).to receive(:staging?).and_return(false)
ENV.delete('NEWS_RSS_PATH')
# 最初のフィードはネットワークエラー
allow_any_instance_of(Object).to receive(:safe_open)
.with('https://news.coderdojo.jp/feed/')
.and_raise(Net::OpenTimeout, 'タイムアウト')
end
it '各エラーをログに記録し、処理を継続する' do
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: タイムアウト/)
expect { fetch_task.invoke }.not_to raise_error
# 空の news.yml が作成される
expect(File.exist?(yaml_path)).to be true
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to eq([])
end
end
end
describe 'エラーリカバリー' do
context 'ネットワークエラー後に再実行した場合' do
before do
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
end
it '正常に処理される' do
# 最初はネットワークエラー
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, 'タイムアウト')
expect { fetch_task.invoke }.not_to raise_error
# エラー時は空のYAMLが作成される
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news']).to eq([])
# safe_openのモックを解除して正常動作に戻す
allow_any_instance_of(Object).to receive(:safe_open).and_call_original
# タスクを再実行可能にする
fetch_task.reenable
# 再実行すると正常に処理される
expect { fetch_task.invoke }.not_to raise_error
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
expect(yaml_content['news'].size).to be > 0
end
end
end
end テストケースの詳細説明1. ネットワークエラーのハンドリング
これらのエラーは 2. 不正なRSSのハンドリング
RSSパースエラーも同様に 3. 破損したYAMLファイルのハンドリング
YAMLの構文エラーは 4. 追加のテストケース
実装上の注意点モックの使用
エラーハンドリングの現状現在の実装では:
この違いは意図的なもので、YAMLファイルの破損は重大な問題として扱われています。 改善提案もしYAMLエラーも graceful に処理したい場合は、以下のような修正が考えられます: existing_news = begin
if File.exist?(yaml_path)
YAML.safe_load(File.read(yaml_path), permitted_classes: [Time], aliases: true)['news'] || []
else
[]
end
rescue Psych::SyntaxError, Psych::DisallowedClass => e
logger.warn("⚠️ Failed to load existing YAML: #{e.message}")
[]
end 要望された3つのエラーケースを網羅的にカバーするテストファイルを作成し、PRにプッシュしました。モックを使用してエラー状況を再現し、適切にエラーハンドリングされることを確認できるようになっています。 |
- ネットワークエラー(接続タイムアウト、HTTPエラー、不正なURL)のテスト - 不正なRSS(無効なXML、空のフィード、必須フィールドの欠落)のテスト - 破損したYAMLファイル(構文エラー、不正な構造、許可されていないクラス)のテスト - 複数エラーの同時発生とエラーリカバリーのテストも含む Co-authored-by: nacchan99 <nacchan99@users.noreply.github.com>
- ネットワークエラーやRSS解析エラー時の挙動を確認 - 破損・不正なYAMLファイルのケースを追加 - ロガーをモック化してwarnログの出力を検証
Fixes #1718 (cf. #1704 ,cf. #1577 )
背景
News
モデルに関するテストがまだ不足しているため、 CI の安定化とコード品質向上のために追加テストを実装します。やること
参考
#1704 (comment)