本章では、VB.NET業務システムにおけるテスト設計、品質管理、リファクタリングについて学習します。
業務システムでは、コードが「一度動いた」だけでは十分ではありません。 仕様変更、データ追加、画面改修、DB変更、外部連携変更、OS更新、担当者変更など、 長期運用の中で何度も修正が発生します。
そのたびに不具合が混入してしまうと、利用者の業務停止、データ不整合、手戻り、障害対応の増加につながります。 そのため、VB.NETの業務開発では、実装力だけでなく、テスト設計、品質管理、レビュー、リファクタリングの力が非常に重要です。
本章のゴールは、単にテストコードを書けるようになることではありません。 壊れにくい設計、確認しやすい構造、変更に強いコード、品質を維持する仕組みを理解し、 長期保守に耐えられるVB.NETシステムを作れるようになることです。
1. 業務システムにおけるテストの重要性
テストは、不具合を見つけるためだけの作業ではありません。 仕様を確認し、設計の妥当性を検証し、将来の変更による影響を抑えるための重要な工程です。
特にVB.NETの業務システムでは、Windows Forms、WPF、DB、ファイル、帳票、外部APIなど、 複数の要素が組み合わさって動作することが多いため、テスト観点を整理しないと確認漏れが発生しやすくなります。
テストで確認すべき観点
- 仕様通りに処理されるか
- 入力エラーを正しく検知できるか
- DB登録・更新・削除が正しく行われるか
- 異常発生時にRollbackされるか
- 権限がない操作を防げるか
- ファイル出力内容が正しいか
- 外部APIエラー時に適切に処理できるか
- 画面がフリーズしないか
- ログが適切に出力されるか
- 既存機能に影響が出ていないか
テスト不足で起きやすい問題
- 特定条件だけ登録に失敗する
- エラー時にデータが中途半端に登録される
- 権限のないユーザーが操作できてしまう
- CSV出力の文字コードが合わず文字化けする
- 帳票の金額計算が画面と一致しない
- 一部の画面だけ古いロジックのまま残る
- 修正した機能とは別の機能が壊れる
テストは、開発の最後にまとめて行うものではなく、設計・実装と並行して考えるべきものです。 テストしやすい設計にすることで、結果的にコードの品質も高くなります。
2. テストの種類
業務システムのテストには、さまざまな種類があります。 それぞれ目的が異なるため、どの段階で何を確認するのかを理解することが重要です。
単体テスト
単体テストは、メソッドやクラス単位で処理が正しく動作するかを確認するテストです。 入力チェック、計算ロジック、業務判定、変換処理など、画面やDBに依存しない処理を確認するのに向いています。
結合テスト
結合テストは、複数のクラスや機能を組み合わせて動作確認するテストです。 ServiceとRepository、画面とService、DB更新処理、外部API連携など、実際の連携部分を確認します。
システムテスト
システムテストは、システム全体が要件通りに動作するかを確認するテストです。 実際の業務フローに沿って、ログイン、検索、登録、承認、帳票出力などを通して確認します。
回帰テスト
回帰テストは、修正や機能追加によって既存機能が壊れていないかを確認するテストです。 レガシーVB.NETシステムでは特に重要です。
受入テスト
受入テストは、利用者や顧客が業務要件を満たしているかを確認するテストです。 開発者視点ではなく、実際の利用者視点で確認することが重要です。
テスト種類の整理
| テスト種別 | 目的 | 対象 |
|---|---|---|
| 単体テスト | クラスやメソッド単位の確認 | Validator、Service、計算処理など |
| 結合テスト | 複数機能の連携確認 | Service + Repository、画面 + DBなど |
| システムテスト | システム全体の確認 | 業務フロー全体 |
| 回帰テスト | 既存機能への影響確認 | 修正影響範囲と関連機能 |
| 受入テスト | 利用者要件の確認 | 実業務シナリオ |
3. 単体テストの考え方
単体テストでは、ひとつのクラスやメソッドが期待通りに動作するかを確認します。 特に、入力値に対して結果が明確に決まる処理は、単体テストに向いています。
単体テストしやすい処理
- 入力チェック
- 金額計算
- 日付計算
- ステータス判定
- 権限判定
- CSV行の変換
- DTOからEntityへの変換
- 文字列フォーマット処理
入力チェッククラスの例
Public Class UserValidator
Public Function Validate(dto As UserRegisterDto) As List(Of String)
Dim errors As New List(Of String)()
If String.IsNullOrWhiteSpace(dto.UserName) Then
errors.Add("ユーザー名を入力してください。")
End If
If String.IsNullOrWhiteSpace(dto.Email) Then
errors.Add("メールアドレスを入力してください。")
ElseIf Not dto.Email.Contains("@") Then
errors.Add("メールアドレスの形式が不正です。")
End If
Return errors
End Function
End Class
このようなValidatorクラスは、画面やDBに依存していないため、単体テストしやすい構造です。
単体テストの観点例
| テスト条件 | 期待結果 |
|---|---|
| ユーザー名が空 | ユーザー名必須エラーになる |
| メールアドレスが空 | メールアドレス必須エラーになる |
| メールアドレスに@がない | 形式エラーになる |
| ユーザー名とメールアドレスが正常 | エラーなしになる |
単体テストでは、正常系だけでなく、異常系、境界値、NULL、空文字、最大文字数などを確認することが重要です。
4. MSTest / NUnitの基礎
VB.NETでは、MSTestやNUnitなどのテストフレームワークを使って単体テストを作成できます。 ここでは、基本的な考え方を理解するためにMSTest形式の例を紹介します。
MSTestの基本例
<TestClass>
Public Class UserValidatorTests
<TestMethod>
Public Sub Validate_UserNameIsEmpty_ReturnsError()
' Arrange
Dim validator As New UserValidator()
Dim dto As New UserRegisterDto With {
.UserName = "",
.Email = "test@example.com"
}
' Act
Dim errors = validator.Validate(dto)
' Assert
Assert.IsTrue(errors.Contains("ユーザー名を入力してください。"))
End Sub
End Class
テストコードでは、Arrange、Act、Assertの3つに分けて考えると整理しやすくなります。
Arrange / Act / Assert
| 段階 | 意味 | 内容 |
|---|---|---|
| Arrange | 準備 | テスト対象や入力値を用意する |
| Act | 実行 | テスト対象のメソッドを呼び出す |
| Assert | 検証 | 結果が期待通りか確認する |
NUnitの例
<TestFixture>
Public Class TaxCalculatorTests
<Test>
Public Sub CalculateTax_Amount1000_Returns100()
Dim calculator As New TaxCalculator()
Dim tax = calculator.CalculateTax(1000D, 0.1D)
Assert.AreEqual(100D, tax)
End Sub
End Class
MSTestでもNUnitでも、考え方は同じです。 重要なのは、テスト対象を明確にし、入力と期待結果を分かりやすくすることです。
テスト名の付け方
テストメソッド名は、何を確認しているのかが分かる名前にします。
Validate_UserNameIsEmpty_ReturnsRequiredError
CalculateTotal_WithTax_ReturnsTaxIncludedAmount
CanApprove_UserIsGeneral_ReturnsFalse
ParseCsvLine_InvalidDate_ReturnsError
テスト名が分かりやすいと、失敗したときに原因を特定しやすくなります。
5. モックの考え方
単体テストでは、DB、ファイル、外部APIなどに直接依存するとテストが難しくなります。 このような外部依存を置き換えるために、モックやFakeを利用します。
テストしにくい例
Public Class UserService
Private ReadOnly _repository As New UserRepository()
Public Function GetUserName(userId As Integer) As String
Dim user = _repository.FindById(userId)
Return user.UserName
End Function
End Class
このコードでは、UserServiceがUserRepositoryを直接生成しているため、 テスト時にも実際のDBアクセスが発生してしまいます。
インターフェースで依存を分離する
Public Interface IUserRepository
Function FindById(userId As Integer) As User
End Interface
Public Class UserService
Private ReadOnly _repository As IUserRepository
Public Sub New(repository As IUserRepository)
_repository = repository
End Sub
Public Function GetUserName(userId As Integer) As String
Dim user = _repository.FindById(userId)
Return user.UserName
End Function
End Class
このように、UserServiceが具体的なRepositoryではなくIUserRepositoryに依存することで、 テスト用の実装に差し替えられるようになります。
FakeRepositoryの例
Public Class FakeUserRepository
Implements IUserRepository
Public Function FindById(userId As Integer) As User Implements IUserRepository.FindById
Return New User With {
.UserId = userId,
.UserName = "TestUser"
}
End Function
End Class
Fakeを使ったテスト例
<TestMethod>
Public Sub GetUserName_UserExists_ReturnsUserName()
Dim repository As New FakeUserRepository()
Dim service As New UserService(repository)
Dim userName = service.GetUserName(1)
Assert.AreEqual("TestUser", userName)
End Sub
このように、DBに接続せずにServiceのテストを行えます。 モックやFakeを使うためには、事前に依存性注入しやすい設計にしておくことが重要です。
モック化しやすい依存先
- Repository
- 外部APIクライアント
- メール送信クラス
- ファイル入出力クラス
- 現在日時を返すクラス
- ログ出力クラス
- 認証・認可サービス
6. テストしやすい設計
テストしやすいコードは、保守しやすいコードでもあります。 逆に、テストしにくいコードは、責務が混ざっていたり、外部依存が強かったりすることが多いです。
テストしにくいコードの特徴
- 画面イベントに業務ロジックが大量に書かれている
- Service内でRepositoryを直接Newしている
- DBアクセスと業務判定が同じメソッドに混在している
- 現在日時をDateTime.Nowで直接取得している
- ファイルパスが固定値で書かれている
- Sharedメソッドに処理が集中している
- 戻り値がなく、内部状態だけを変更している
テストしやすいコードの特徴
- 業務ロジックがServiceやDomainクラスに分離されている
- DBアクセスはRepositoryに分離されている
- 外部依存はインターフェース経由で注入されている
- 入力と出力が明確である
- 副作用が少ない
- 現在日時や設定値を外部から渡せる
- 小さなメソッドに分割されている
現在日時を直接使う悪い例
Public Function IsExpired(limitDate As DateTime) As Boolean
Return DateTime.Now > limitDate
End Function
このコードは現在時刻に依存しているため、テスト時に結果が変わる可能性があります。
現在日時を外部から渡す例
Public Function IsExpired(limitDate As DateTime, currentDateTime As DateTime) As Boolean
Return currentDateTime > limitDate
End Function
このように現在日時を引数で渡すことで、テスト時に任意の日時を指定できます。
テストしやすい設計の基本方針
- 画面とロジックを分離する
- DBや外部APIへの依存を直接持たない
- インターフェースを使って差し替え可能にする
- 1メソッドに複数の責務を持たせない
- 戻り値や例外で結果を明確に表現する
- 固定値や環境依存値を外部化する
7. レガシーVBコードの改善
VB.NETの業務システムでは、長年運用されてきたレガシーコードを保守する場面が多くあります。 レガシーコードには、画面クラスの肥大化、巨大な共通関数、暗黙的な型変換、SQL直書き、例外握りつぶしなどが含まれていることがあります。
レガシーVBコードでよくある問題
- 1つのFormクラスが数千行ある
- ボタンクリックイベントにSQLと業務ロジックが混在している
- Option Strict Offのまま開発されている
- Object型やVariant的な使い方が多い
- DataTableを全層で使い回している
- エラー時にCatchして何もしない
- 同じような処理が複数画面にコピペされている
- 関数名や変数名から役割が分からない
改善の進め方
1. いきなり全面改修しない
2. まず現状の動作を把握する
3. 重要な機能からテスト観点を整理する
4. 変更頻度が高い箇所から改善する
5. 小さな単位でメソッド抽出する
6. DBアクセスをRepositoryへ分離する
7. 入力チェックをValidatorへ分離する
8. 既存動作を変えないように確認する
9. 改善後に回帰テストを行う
レガシーコード改善で最も危険なのは、動作を理解しないまま大きく書き換えることです。 まずは現在の仕様と実際の挙動を把握し、小さく安全に改善することが重要です。
メソッド抽出の例
長いイベントハンドラから、入力チェック処理を別メソッドへ切り出します。
Private Function ValidateInput() As List(Of String)
Dim errors As New List(Of String)()
If String.IsNullOrWhiteSpace(txtUserName.Text) Then
errors.Add("ユーザー名を入力してください。")
End If
If String.IsNullOrWhiteSpace(txtEmail.Text) Then
errors.Add("メールアドレスを入力してください。")
End If
Return errors
End Function
最初から完璧な設計にする必要はありません。 まずは長すぎる処理を意味のある単位に分けるだけでも、読みやすさと保守性は向上します。
8. コードスメル
コードスメルとは、すぐにバグではないものの、将来的に保守性や品質を悪化させる可能性があるコード上の兆候です。
コードスメルを見つけられるようになると、リファクタリングすべき箇所を判断しやすくなります。
代表的なコードスメル
| コードスメル | 内容 | 改善方針 |
|---|---|---|
| 巨大クラス | 1つのクラスが多くの責務を持つ | 責務ごとにクラス分割する |
| 巨大メソッド | 1つのメソッドが長すぎる | 意味のある単位でメソッド抽出する |
| 重複コード | 同じ処理が複数箇所にある | 共通化する |
| 長すぎる引数 | メソッド引数が多すぎる | DTOやParameter Objectを使う |
| マジックナンバー | 意味不明な数値や文字列が直接書かれている | 定数やEnumにする |
| 深いネスト | IfやForが何重にも入れ子になっている | 早期Returnやメソッド抽出を使う |
| 責務不明な共通クラス | CommonやUtilに何でも入っている | 目的別のクラスへ分ける |
深いネストの悪い例
If user IsNot Nothing Then
If user.IsActive Then
If user.Role = UserRole.Admin Then
ExecuteAdminProcess()
End If
End If
End If
早期Returnで改善した例
If user Is Nothing Then
Return
End If
If Not user.IsActive Then
Return
End If
If user.Role <> UserRole.Admin Then
Return
End If
ExecuteAdminProcess()
ネストを浅くすると、条件が読みやすくなり、処理の流れも追いやすくなります。
9. リファクタリング手法
リファクタリングとは、外部から見た動作を変えずに、内部構造を改善することです。 不具合修正や機能追加とは異なり、目的はコードの読みやすさ、保守性、拡張性を高めることです。
リファクタリングの基本原則
- 動作を変えない
- 小さな単位で行う
- 変更前後でテストする
- 目的を明確にする
- 一度に大量修正しない
- リファクタリングと仕様変更を混ぜない
代表的なリファクタリング
- メソッド抽出
- クラス抽出
- 変数名の改善
- 定数化
- 重複コードの共通化
- 条件式の分解
- DTOの導入
- Repository分離
- Validator分離
マジックナンバーの改善例
If amount >= 100000 Then
discountRate = 0.1D
End If
このコードでは、100000や0.1Dの意味が分かりにくいです。
Private Const DiscountThreshold As Decimal = 100000D
Private Const PremiumDiscountRate As Decimal = 0.1D
If amount >= DiscountThreshold Then
discountRate = PremiumDiscountRate
End If
定数化することで、値の意味が明確になります。 また、同じ値を複数箇所で使っている場合も変更しやすくなります。
長い引数の改善例
Public Sub RegisterUser(userName As String, email As String, departmentId As Integer, roleId As Integer, startDate As DateTime)
' 登録処理
End Sub
引数が多いメソッドは、呼び出し時に値の順序を間違えやすくなります。
Public Class UserRegisterRequest
Public Property UserName As String
Public Property Email As String
Public Property DepartmentId As Integer
Public Property RoleId As Integer
Public Property StartDate As DateTime
End Class
Public Sub RegisterUser(request As UserRegisterRequest)
' 登録処理
End Sub
DTOを使うことで、引数の意味が明確になり、将来的に項目が増えた場合も対応しやすくなります。
10. 静的解析
静的解析とは、プログラムを実行せずにソースコードを解析し、 潜在的な問題を検出する仕組みです。
コンパイルエラーだけでは検出できないコード品質上の問題、 未使用変数、命名規則違反、複雑すぎるメソッド、例外処理不足などを発見するのに役立ちます。
静的解析で検出できる問題
- 未使用変数
- 未使用メソッド
- 到達不能コード
- 重複コード
- 複雑すぎる条件分岐
- 命名規則違反
- Option Strict Offによる危険な型変換
- 例外の握りつぶし
- SQL文字列連結の疑い
VB.NETで意識したい設定
Option Strict On
Option Explicit On
Option Infer On
Option Strict Onにすることで、暗黙的な縮小変換や遅延バインディングを防げます。 Option Explicit Onにすることで、未宣言変数の利用を防げます。 Option Infer Onは、型推論を適切に使うことでコードを簡潔にできます。
静的解析の活用ポイント
- 警告を放置しない
- チームでルールを統一する
- 新規コードから段階的に適用する
- 既存レガシーコードには一括適用せず優先度を決める
- レビュー前に自動チェックする
静的解析は、開発者を責めるためのものではありません。 人間が見落としやすい問題を機械的に検出し、品質を安定させるための仕組みです。
11. 品質指標とレビュー観点
品質管理では、感覚だけで「良いコード」「悪いコード」を判断するのではなく、 一定の観点や指標を使って確認することが重要です。
代表的な品質指標
- 不具合件数
- テストケース数
- テスト消化率
- テスト合格率
- コードカバレッジ
- 循環的複雑度
- 重複コード率
- レビュー指摘件数
- 障害発生件数
- 修正後の再発件数
コードカバレッジが高ければ必ず品質が高いわけではありません。 しかし、重要な業務ロジックにまったくテストがない状態は危険です。 数値は目的ではなく、品質を把握するための材料として使います。
コードレビューの観点
- 仕様を満たしているか
- 入力チェックが不足していないか
- 例外処理が適切か
- ログが必要な箇所に出ているか
- SQLインジェクション対策ができているか
- トランザクション制御が必要な箇所にあるか
- 権限チェックが画面だけでなくService側にもあるか
- メソッドやクラスの責務が明確か
- 重複コードがないか
- 変数名・メソッド名が意味を表しているか
- テストしやすい構造か
レビューで避けるべきこと
- 好みだけで指摘する
- 命名やフォーマットだけに偏る
- 仕様確認をせずコードだけを見る
- 指摘理由を説明しない
- 大きすぎる差分を一度にレビューする
コードレビューは、バグを見つけるだけでなく、設計意図を共有し、チーム全体の品質基準をそろえるための場でもあります。
12. テストケース設計
テストケースを設計する際は、正常系だけではなく、異常系、境界値、権限、状態遷移、データパターンを考慮する必要があります。
テストケースに含める項目
| 項目 | 内容 |
|---|---|
| テストケースID | 一意に識別する番号 |
| 機能名 | 対象機能 |
| テスト観点 | 何を確認するか |
| 前提条件 | 事前に必要な状態やデータ |
| 入力値 | 画面入力、ファイル内容、APIリクエストなど |
| 操作手順 | 実際に行う操作 |
| 期待結果 | 正しい結果 |
| 実施結果 | OK / NG |
| 備考 | 補足情報 |
境界値テストの例
数量が1以上999以下という仕様の場合、以下のような値を確認します。
| 入力値 | 期待結果 |
|---|---|
| 0 | エラー |
| 1 | 正常 |
| 999 | 正常 |
| 1000 | エラー |
状態遷移テストの例
申請データのステータスが「下書き」「申請中」「承認済み」「差戻し」のように変化する場合、 許可される操作と禁止される操作を整理します。
| 現在の状態 | 操作 | 期待結果 |
|---|---|---|
| 下書き | 申請 | 申請中になる |
| 申請中 | 承認 | 承認済みになる |
| 承認済み | 編集 | エラーになる |
| 差戻し | 再申請 | 申請中になる |
業務システムでは、単純な入力値だけでなく、データの状態によって処理可否が変わることが多いため、 状態遷移のテストが非常に重要です。
13. 実務でありがちな品質管理の問題
正常系しかテストしていない
正常な入力だけでテストしていると、異常系や境界値で不具合が残ります。 実務では、入力ミス、権限不足、DBエラー、外部APIエラーなども確認する必要があります。
テスト観点が人によってバラバラ
担当者ごとに確認観点が異なると、品質にばらつきが出ます。 入力チェック、権限、DB更新、ログ、例外処理など、共通のレビュー観点やテスト観点を用意することが重要です。
修正後に関連機能を確認していない
一部機能を修正した際に、関連する既存機能を確認しないと、回帰不具合が発生します。 影響範囲を整理し、回帰テストを行う必要があります。
テストデータが不足している
実際の業務では、NULL、空文字、長い文字列、古いデータ、異常なステータスなどが存在します。 きれいなデータだけでテストすると、本番で想定外の不具合が起きやすくなります。
リファクタリングと仕様変更を同時に行っている
リファクタリングと仕様変更を同時に行うと、不具合が発生したときに原因を特定しにくくなります。 可能な限り、内部改善と機能変更は分けて行います。
テストコードが保守されていない
テストコードもプロダクトコードと同じく保守対象です。 仕様変更後にテストコードを更新しないと、テストが信頼できないものになります。
14. 演習課題
演習1:入力チェックの単体テストを作成する
UserValidatorクラスに対して、ユーザー名未入力、メールアドレス未入力、メール形式不正、正常入力のテストを作成してください。
演習2:権限判定の単体テストを作成する
一般ユーザー、承認者、管理者のロールを用意し、 承認操作やユーザー管理操作が可能かどうかをテストしてください。
演習3:Repository依存をFakeに差し替える
UserServiceがIUserRepositoryに依存するように修正し、 テスト時にはFakeUserRepositoryを渡してDBに接続せずにテストできるようにしてください。
演習4:巨大メソッドをリファクタリングする
入力チェック、DB登録、メール送信、ログ出力が1つにまとまったメソッドを、 責務ごとに分割してください。
演習5:テストケース表を作成する
ユーザー登録機能を対象に、正常系、必須チェック、文字数チェック、重複チェック、DBエラーのテストケースを設計してください。
演習6:コードスメルを検出する
既存のVB.NETコードを読み、巨大メソッド、重複コード、マジックナンバー、深いネスト、責務不明な共通クラスを探してください。 それぞれに対して改善方針を整理してください。
演習7:回帰テスト観点を整理する
ユーザー登録処理にメールアドレス重複チェックを追加した場合、 どの既存機能を回帰テストすべきか整理してください。
15. まとめ
本章では、VB.NET業務システムにおけるテスト設計、品質管理、リファクタリングについて学習しました。
業務システムでは、コードが一度動くだけでは十分ではありません。 長期運用の中で仕様変更や機能追加が発生するため、壊れにくく、確認しやすく、変更に強いコードを作る必要があります。
単体テストでは、入力チェック、計算処理、権限判定、状態判定、変換処理など、 画面やDBに依存しないロジックを確認します。 MSTestやNUnitを利用することで、テストを自動化し、変更時の影響を検知しやすくなります。
テストしやすい設計にするためには、画面、Service、Repository、Validator、外部連携処理を適切に分離することが重要です。 DBや外部APIなどの依存先は、インターフェースを通じて差し替えられるようにすると、 モックやFakeを使ったテストが行いやすくなります。
レガシーVBコードを改善する場合は、いきなり全面改修するのではなく、 現状の動作を把握し、小さな単位で安全にリファクタリングすることが重要です。 特に、巨大メソッド、重複コード、深いネスト、マジックナンバー、責務不明な共通クラスは改善対象になりやすいです。
品質管理では、テストケース、レビュー観点、静的解析、コードカバレッジ、回帰テストなどを組み合わせて、 属人的ではない品質保証の仕組みを作る必要があります。 ただし、数値だけを目的にするのではなく、実際の業務リスクを減らすことを重視します。
本章のゴールは、単にテストを書くことではなく、テストしやすく、壊れにくく、保守しやすいVB.NETシステムを設計できるようになることです。 この考え方は、次章のレガシーVBシステムの移行・モダナイゼーションにも直結します。