「リファクタリング時にユニットテストを機能させ続けるにはどうすればよいですか?」など、同じような質問に答えて読んだことがあります。私の場合、私が持っているいくつかの標準にレビューしてそれに合わせるためのプロジェクトが与えられたという点で、シナリオは少し異なります。現在、プロジェクトに対するテストはまったくありません!
サービスレイヤーでDAOタイプのコードを混在させないなど、もっとうまくできたと思われる多くのことを確認しました。
リファクタリングする前に、既存のコードのテストを書くことは良い考えのように思えました。私が考えている問題は、リファクタリングを実行すると、特定のロジックが実行されている場所を変更するときにこれらのテストが中断し、テストが前の構造(モックされた依存関係など)を考慮して記述されることです
私の場合、続行するにはどの方法が最適ですか?私はリファクタリングされたコードを中心にテストを記述したくなりますが、望ましい動作を変更する可能性のあるものを誤ってリファクタリングするリスクがあることを認識しています。
これがリファクタリングかリデザインかに関わらず、修正すべきこれらの用語を理解できて嬉しく思います。現在、リファクタリングについて次の定義に取り組んでいます。あなたはそれをどのように行うかを変更します。」だから私はソフトウェアが何をするかを変えるつもりはありません。
同様に、私は、メソッドのシグネチャを変更する場合、再設計と見なすことができるという議論を見ることができます。
ここに簡単な例があります
MyDocumentService.Java
(電流)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.Java
(何でもリファクタリング/再設計)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
regressionsをチェックするテストを探しています。つまり、既存の動作の一部を壊します。まず、その振る舞いがどのレベルで維持され、その振る舞いを駆動するインターフェースも同じままであることを特定し、その時点でテストを開始します。
これで、何をしても以下このレベルでも、動作は同じであると断言するテストがいくつかあります。
テストとコードの同期を維持する方法については、疑問の余地があります。コンポーネントへのinterfaceが変わらない場合は、これについてテストを記述し、両方の実装に対して同じ条件をアサートできます(新しい実装を作成するときに)。そうでない場合は、冗長コンポーネントのテストが冗長テストであることを受け入れる必要があります。
推奨される方法は、バグを含む可能性があるコードの現在の動作をテストする「ピンダウンテスト」を書くことから始めることですが、要件ドキュメントに違反する特定の動作がバグであるかどうかを見極めるという狂気に降りる必要はありません。あなたが気づいていない何かのための回避策、または要件の文書化されていない変更を表します。
これらのピンダウンテストが高レベルであること、つまりユニットテストではなく統合であることは最も理にかなっているため、リファクタリングを開始しても機能し続けます。
ただし、コードをテスト可能にするには、いくつかのリファクタリングが必要になる場合があります。「安全な」リファクタリングに固執するように注意してください。たとえば、ほとんどすべての場合、プライベートなメソッドは何も壊すことなくパブリックにできます。
レガシーコードを効果的に使用する と リファクタリング-既存のコードのデザインを改善する の両方を読んでいない場合は、お勧めします。
[..]私に思われる問題は、リファクタリングを行うと、特定のロジックが実行される場所を変更するときにこれらのテストが中断し、テストが前の構造(モックされた依存関係など)を考慮して書き込まれることです。 ..]
私は必ずしもこれを問題と見なしているわけではありません。テストを記述し、コードの構造を変更してから、テスト構造を調整しますalso。これは、新しい構造が実際に古い構造よりも優れているかどうかを直接フィードバックします。そうであれば、調整されたテストはeasierになるため(したがって、テストの変更は比較的簡単で、新しく導入されたバグがテストに合格するリスクを低くする必要があります)。
また、他の人がすでに書いているように:too詳細なテストを記述しないでください(少なくとも最初は)。高レベルの抽象化を維持するようにしてください(したがって、テストはおそらく回帰テストまたは統合テストとしてもより適切に特徴付けられます)。
すべての依存関係を模擬する厳密な単体テストを記述しないでください。一部の人々は、これらは実際の単体テストではないとあなたに言うでしょう。それらを無視してください。これらのテストは有用であり、それが重要です。
あなたの例を見てみましょう:
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
テストはおそらく次のようになります。
DocumentDao documentDao = Mock.create(DocumentDao.class);
Mock.when(documentDao.findAllDocuments())
.thenReturn(DataResultSet.create(...))
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());
DocumentDaoをモックする代わりに、その依存関係をモックします。
DocumentDao documentDao = new DocumentDao(db);
Mock.when(db...)
.thenReturn(...)
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());
これで、テストを中断することなく、ロジックをMyDocumentService
からDocumentDao
に移動できます。テストでは、機能が同じであることを示します(テストした限り)。
tl; dr単体テストを記述しません。より適切なレベルでテストを記述します。
あなたのリファクタリングの実際の定義を考えると:
ソフトウェアの機能を変更するのではなく、ソフトウェアの機能を変更します
非常に広いスペクトルがあります。一端は、おそらくより効率的なアルゴリズムを使用して、特定のメソッドへの自己完結型の変更です。もう一方の端は別の言語に移植しています。
実行されているリファクタリング/再設計のレベルに関係なく、そのレベル以上で動作するテストを行うことが重要です。
自動テストは多くの場合、レベルによって次のように分類されます。
単体テスト-個々のコンポーネント(クラス、メソッド)
統合テスト-コンポーネント間の相互作用
システムテスト-完全なアプリケーション
本質的に変更されていないリファクタリングに耐えられるテストのレベルを記述します。
考える:
アプリケーションがbeforeとafterリファクタリング? どうすれば同じように機能するかテストできますか?
あなたが言うように、あなたがふるまいを変えるならば、それは変換であり、リファクタリングではありません。どのレベルで動作を変更すると、違いが生まれます。
最高レベルの正式なテストがない場合は、コード(機能していると見なされるように再設計した後も同じままでいる必要があるクライアント(コードまたは人間を呼び出す)が必要とする一連の要件を見つけてください。これは、実装する必要があるテストケースのリストです。
テストケースの変更を必要とする実装の変更に関する質問に対処するには、デトロイト(クラシック)とロンドン(モック奏者)のTDDを確認することをお勧めします。マーティン・ファウラーは彼の素晴らしい記事でこれについて話します モックはスタブではありません しかし、多くの人々が意見を持っています。外部が変更できない最高レベルから始めて、下に進むと、本当に変更する必要があるレベルに到達するまで、要件はかなり安定しているはずです。
テストがなければ、これは難しくなります。新しいコードが必要なことを確実に実行できるようになるまで、デュアルコードパスを介してクライアントを実行する(および違いを記録する)ことを検討することをお勧めします。
ここに私のアプローチ。 4段階のリファクタリングテストであるため、時間の面でコストがかかります。
私が公開しようとしているものは、質問の例で公開されているものよりも複雑なコンポーネントに適しています。
とにかく、戦略は、コンポーネント候補(インターフェース(DAO、サービス、コントローラーなど))によって正規化される場合に有効です。
MyDocumentServiceからすべてのパブリックメソッドを収集して、それらをすべてインターフェイスにまとめることができます。例えば。 それがすでに存在する場合は、新しいものを設定する代わりにそれを使用します。
public interface DocumentService {
List<Document> getAllDocuments();
//more methods here...
}
次に、MyDocumentServiceにこの新しいインターフェイスの実装を強制します。
ここまでは順調ですね。大きな変更は行われませんでした。現在の契約を尊重し、behaivosはそのままです。
public class MyDocumentService implements DocumentService {
@Override
public List<Document> getAllDocuments(){
//legacy code here as it is.
// with no changes ...
}
}
ここで私たちは大変な仕事をしています。テストスイートをセットアップします。可能な限り多くのケースを設定する必要があります。成功したケースとエラーケースです。これらの最後は、結果の品質のためです。
ここで、MyDocumentServiceをテストする代わりに、テストするコントラクトとしてインターフェイスを使用します。
詳細については説明しませんので、コードが単純すぎるか不可知論的であるように見える場合はご容赦ください
public class DocumentServiceTestSuite {
@Mock
MyDependencyA mockDepA;
@Mock
MyDependencyB mockDepB;
//... More mocks
DocumentService service;
@Before
public void initService(){
service = MyDocumentService(mockDepA, mockDepB);
//this is purposed way to inject
//dependencies. Replace it with one you like more.
}
@Test
public void getAllDocumentsOK(){
// here I mock depA and depB
// wanted behaivors...
List<Document> result = service.getAllDocuments();
Assert.assertX(result);
Assert.assertY(result);
//... As many you think appropiate
}
}
この方法では、この段階が他のどの段階よりも長くかかります。そして、それは将来の比較のための参照のポイントを設定するので、それは最も重要です。
注:大きな変更が加えられていないため、行動に影響はありません。ここでSCMにタグを付けることをお勧めします。タグやブランチは関係ありません。バージョンを実行するだけです。
これは、ロールバック、バージョン比較のために必要であり、古いコードと新しいコードの並列実行のためかもしれません。
リファクタリングは新しいコンポーネントに実装される予定です。既存のコードは変更しません。最初のステップは、MyDocumentServiceをコピーして貼り付け、CustomDocumentService(たとえば)に名前を変更するのと同じくらい簡単です。
新しいクラスはDocumentServiceを実装し続けます。次に、getAllDocuments()に移動してリファクタリングします。 (1から始めましょう。ピンリファクタリング)
DAOのインターフェイス/メソッドでいくつかの変更が必要になる場合があります。その場合は、既存のコードを変更しないでください。 DAOインターフェースに独自のメソッドを実装します。古いコードにDeprecatedと注釈を付けると、後で何を削除すべきかがわかります。
既存の実装を壊したり変更したりしないことが重要です。両方のservicesを並行して実行し、結果を比較します。
public class CustomDocumentService implements DocumentService {
@Override
public List<Document> getAllDocuments(){
//new code here ...
//due to im refactoring service
//I do the less changes possible on its dependencies (DAO).
//these changes will come later
//and they will have their own tests
}
}
わかりました、今は簡単な部分です。新しいコンポーネントのテストを追加します。
public class DocumentServiceTestSuite {
@Mock
MyDependencyA mockDepA;
@Mock
MyDependencyB mockDepB;
DocumentService service;
DocumentService customService;
@Before
public void initService(){
service = MyDocumentService(mockDepA, mockDepB);
customService = CustomDocumentService(mockDepA, mockDepB);
// this is purposed way to inject
//dependencies. Replace it with the one you like more
}
@Test
public void getAllDocumentsOK(){
// here I mock depA and depB
// wanted behaivors...
List<Document> oldResult = service.getAllDocuments();
Assert.assertX(oldResult);
Assert.assertY(oldResult);
//... As many you think appropiate
List<Document> newResult = customService.getAllDocuments();
Assert.assertX(newResult);
Assert.assertY(newResult);
//... The very same made to oldResult
//this is optional
Assert.assertEquals(oldResult,newResult);
}
}
これでoldResultとnewResultの両方が個別に検証されましたが、互いに比較することもできます。この最後の検証はオプションであり、結果に依存します。比較できないかもしれません。
この方法で2つのコレクションを比較するにはあまり見かけはしないかもしれませんが、他の種類のオブジェクト(ポジョ、データモデルエンティティ、DTO、ラッパー、ネイティブタイプ...)に対しては有効です。
メモ
ユニットテストの方法やモックライブラリの使用方法については、あえて説明しません。どのようにリファクタリングを行う必要があるかについても、私はあえて言いません。私がやりたかったのは、グローバル戦略を提案することです。それを進める方法はあなた次第です。コードがどのように複雑であるか、そしてそのような戦略を試す価値があるかどうかを正確に知っています。ここでは、時間やリソースなどの事実が重要です。また、今後これらのテストから何を期待するかが重要です。
私はサービスからサンプルを開始し、DAOなどをフォローします。依存関係レベルに深く入ります。多かれ少なかれ、それはp-bottom戦略として説明できます。ただし、マイナーな変更/リファクタリング(ツアーの例で公開されているもの)の場合、bottom upを使用すると作業が簡単になります。変更の範囲が少ないからです。
最後に、非推奨のコードを削除し、古い依存関係を新しい依存関係にリダイレクトするのはあなた次第です。
非推奨のテストも削除して、ジョブを実行します。テストで古いソリューションをバージョン管理した場合は、いつでもお互いを確認して比較できます。
非常に多くの作業の結果として、レガシーコードがテスト、検証、バージョン管理されています。そして、新しいコードがテストされ、検証され、バージョン管理の準備が整います。
インターフェイスが重要な方法で変更されると予想できるポイントでフックするテストを作成する時間を無駄にしないでください。これは、多くの場合、本質的に「協調的」であるクラスを単体テストしようとしている兆候です。その価値は、クラス自体が行うことではなく、密接に関連するいくつかのクラスと相互作用して貴重な動作を生成する方法にあります。 。テストしたいのはthat動作です。つまり、より高いレベルでテストしたいということです。このレベルを下回るテストは、多くの醜いモックを必要とすることが多く、結果として生じるテストは、振る舞いを防御するための支援というより、開発の妨げになる可能性があります。
リファクタリング、再設計などを行うことに夢中になる必要はありません。低いレベルで多くのコンポーネントの再設計を構成する変更を加えることができますが、より高い統合レベルでは、単純にリファクタリングになります。重要なのは、どのような行動があなたにとって価値があるのかを明確にし、あなたが行くときにその行動を守ることです。
テストを作成する際に検討すると役立つ場合があります。QA、製品の所有者、またはユーザーに、このテストが実際にテストしていることを簡単に説明できますか?テストの記述が難解で技術的すぎると思われる場合は、おそらく間違ったレベルでテストしていることになります。 「理にかなっている」ポイント/レベルでテストし、すべてのレベルのテストでコードをこじらせないでください。
最初のタスクは、テストの「理想的なメソッドシグネチャ」を考え出すことです。 純粋な関数 になるように努力してください。これは、実際にテストされているコードとは無関係である必要があります。これは小さなアダプター層です。このアダプターレイヤーにコードを記述します。コードをリファクタリングするときは、アダプターレイヤーを変更するだけで済みます。以下に簡単な例を示します。
[TestMethod]
public void simple_addition()
{
Assert.AreEqual(7, Eval("3 + 4"));
}
[TestMethod]
public void order_of_operations()
{
Assert.AreEqual(52, Eval("2 + 5 * 10"));
}
[TestMethod]
public void absolute_value()
{
Assert.AreEqual(9, Eval("abs(-9)"));
Assert.AreEqual(5, Eval("abs(5)"));
Assert.AreEqual(0, Eval("abs(0)"));
}
static object Eval(string expression)
{
// This is the code under test.
// I can refactor this as much as I want without changing the tests.
var settings = new EvaluatorSettings();
Evaluator.Settings = settings;
Evaluator.Evaluate(expression);
return Evaluator.LastResult;
}
テストは適切ですが、テスト中のコードには不適切なAPIがあります。アダプターレイヤーを更新するだけで、テストを変更せずにリファクタリングできます。
static object Eval(string expression)
{
// After refactoring...
var settings = new EvaluatorSettings();
var evaluator = new Evaluator(settings);
return evaluator.Evaluate(expression);
}
この例は、Do n't Repeat Yourselfの原則に従ってかなり明白なことのように見えますが、他の場合ではそれほど明白ではない場合があります。利点は、DRYを超えています-実際の利点は、テスト対象のコードからテストを切り離すことです。
もちろん、この手法はすべての状況で推奨されるとは限りません。たとえば、POCO/POJOのアダプターを作成する理由はありません。テストコードとは独立して変更できるAPIが実際にはないからです。また、少数のテストを作成している場合は、比較的大きなアダプターレイヤーはおそらく無駄な作業になります。