MSTest

テストユニットテストC#.NETMicrosoftVisual Studio

テストツール

MSTest

概要

MSTestはMicrosoftが開発する.NETアプリケーション用の公式テストフレームワークです。Visual StudioとVisual Studio Code Test Explorer、.NET CLI、多数のCIパイプラインとの統合を提供し、.NET Framework、.NET Core、.NET、UWP、WinUIなど、すべてのサポートされる.NETターゲットで動作します。オープンソースでクロスプラットフォーム対応の成熟したテストフレームワークとして、企業開発において広く採用されています。

詳細

MSTestの最大の特徴は、Microsoft開発エコシステムとの深い統合にあります。Visual Studioでのテスト実行とデバッグ体験が非常に優れており、IntelliSenseサポートやテストエクスプローラーとの連携により、効率的なテスト開発が可能です。2024年のMSTest 3.8では、Microsoft.Testing.Platform(MTP)への統合、フィルタリング機能の向上、協調的キャンセレーション機能などが追加されています。

属性ベースのテスト定義では、[TestClass]、[TestMethod]、[DataRow]、[TestInitialize]、[TestCleanup]など豊富な属性を提供し、テストライフサイクルを細かく制御できます。並列実行機能により、クラスレベルやメソッドレベルでの並行テスト実行が可能で、大規模なテストスイートでも高速な実行を実現できます。

メリット・デメリット

メリット

  • Visual Studio統合: 最高クラスのIDE統合とデバッグ体験
  • Microsoft公式: .NETエコシステムでの安定性と長期サポート
  • 豊富な属性: テストライフサイクルを制御する包括的な属性セット
  • データ駆動テスト: DataRowやDynamicDataによる柔軟なパラメータ化
  • 並列実行: クラス・メソッドレベルでの並行実行サポート
  • Enterprise対応: 大規模プロジェクトでの実績と信頼性

デメリット

  • Microsoft依存: .NET以外の環境では使用不可
  • 設定の複雑さ: 高度な機能を使用する際の設定が複雑
  • 柔軟性の制限: 他フレームワークと比べてカスタマイズ性が低い
  • 学習コスト: Microsoft特有の概念や命名規則の習得が必要

参考ページ

書き方の例

Hello World

// MSTest.Sdk プロジェクトファイル
<Project Sdk="MSTest.Sdk/3.8.3">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
</Project>

// HelloWorldTest.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class HelloWorldTest
{
    [TestMethod]
    public void TestGreeting()
    {
        string result = Greet("MSTest");
        Assert.AreEqual("Hello, MSTest!", result);
    }
    
    private string Greet(string name)
    {
        return $"Hello, {name}!";
    }
}

基本テスト

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CalculatorTest
{
    private Calculator _calculator;
    
    [TestInitialize]
    public void Setup()
    {
        _calculator = new Calculator();
    }
    
    [TestMethod]
    public void Add_PositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 5;
        int b = 3;
        
        // Act
        int result = _calculator.Add(a, b);
        
        // Assert
        Assert.AreEqual(8, result);
    }
    
    [TestMethod]
    public void Add_NegativeNumbers_ReturnsCorrectSum()
    {
        // Arrange & Act
        int result = _calculator.Add(-2, -3);
        
        // Assert
        Assert.AreEqual(-5, result);
    }
    
    [TestMethod]
    [ExpectedException(typeof(DivideByZeroException))]
    public void Divide_ByZero_ThrowsException()
    {
        // Act
        _calculator.Divide(10, 0);
    }
    
    [TestMethod]
    [Timeout(2000)]
    public void ComplexCalculation_CompletesWithinTimeout()
    {
        // Act
        var result = _calculator.ComplexCalculation();
        
        // Assert
        Assert.IsNotNull(result);
    }
    
    [TestCleanup]
    public void Cleanup()
    {
        _calculator?.Dispose();
    }
}

モック

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

[TestClass]
public class UserServiceTest
{
    private Mock<IUserRepository> _mockRepository;
    private UserService _userService;
    
    [TestInitialize]
    public void Setup()
    {
        _mockRepository = new Mock<IUserRepository>();
        _userService = new UserService(_mockRepository.Object);
    }
    
    [TestMethod]
    public void GetUser_ValidId_ReturnsUser()
    {
        // Arrange
        var expectedUser = new User { Id = 1, Name = "John Doe" };
        _mockRepository.Setup(r => r.GetById(1))
                      .Returns(expectedUser);
        
        // Act
        var result = _userService.GetUser(1);
        
        // Assert
        Assert.IsNotNull(result);
        Assert.AreEqual("John Doe", result.Name);
        _mockRepository.Verify(r => r.GetById(1), Times.Once);
    }
    
    [TestMethod]
    public void CreateUser_ValidUser_CallsRepository()
    {
        // Arrange
        var newUser = new User { Name = "Jane Smith" };
        var savedUser = new User { Id = 2, Name = "Jane Smith" };
        
        _mockRepository.Setup(r => r.Save(It.IsAny<User>()))
                      .Returns(savedUser);
        
        // Act
        var result = _userService.CreateUser(newUser);
        
        // Assert
        Assert.AreEqual(2, result.Id);
        Assert.AreEqual("Jane Smith", result.Name);
        _mockRepository.Verify(r => r.Save(It.IsAny<User>()), Times.Once);
    }
}

非同期テスト

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;

[TestClass]
public class AsyncOperationTest
{
    [TestMethod]
    public async Task ProcessDataAsync_ValidInput_ReturnsProcessedData()
    {
        // Arrange
        var processor = new DataProcessor();
        var inputData = "test data";
        
        // Act
        var result = await processor.ProcessDataAsync(inputData);
        
        // Assert
        Assert.IsNotNull(result);
        Assert.IsTrue(result.Contains("processed"));
    }
    
    [TestMethod]
    public async Task DownloadFileAsync_LargeFile_CompletesSuccessfully()
    {
        // Arrange
        var downloader = new FileDownloader();
        var url = "https://example.com/largefile.zip";
        
        // Act
        var success = await downloader.DownloadAsync(url);
        
        // Assert
        Assert.IsTrue(success);
    }
    
    [TestMethod]
    [Timeout(5000)]
    public async Task SlowOperationAsync_WithTimeout_CompletesWithinTimeLimit()
    {
        // Arrange
        var service = new SlowService();
        
        // Act
        var result = await service.ProcessSlowlyAsync();
        
        // Assert
        Assert.IsNotNull(result);
    }
}

セットアップ・ティアダウン

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class DatabaseIntegrationTest
{
    private static DatabaseContext _context;
    private TestDataBuilder _dataBuilder;
    
    [ClassInitialize]
    public static void ClassSetup(TestContext context)
    {
        // テストクラス全体の初期化
        _context = new DatabaseContext("test-connection-string");
        _context.Database.EnsureCreated();
    }
    
    [ClassCleanup]
    public static void ClassTeardown()
    {
        // テストクラス全体のクリーンアップ
        _context?.Database.EnsureDeleted();
        _context?.Dispose();
    }
    
    [TestInitialize]
    public void Setup()
    {
        // 各テストメソッドの前処理
        _dataBuilder = new TestDataBuilder(_context);
        _dataBuilder.SeedTestData();
    }
    
    [TestCleanup]
    public void Cleanup()
    {
        // 各テストメソッドの後処理
        _dataBuilder?.CleanTestData();
    }
    
    [TestMethod]
    public void SaveUser_ValidUser_SavesSuccessfully()
    {
        // Arrange
        var user = new User { Name = "Test User", Email = "[email protected]" };
        
        // Act
        _context.Users.Add(user);
        _context.SaveChanges();
        
        // Assert
        var savedUser = _context.Users.FirstOrDefault(u => u.Email == "[email protected]");
        Assert.IsNotNull(savedUser);
        Assert.AreEqual("Test User", savedUser.Name);
    }
    
    [TestMethod]
    public void GetUsers_WithData_ReturnsAllUsers()
    {
        // Act
        var users = _context.Users.ToList();
        
        // Assert
        Assert.IsTrue(users.Count > 0);
    }
}

高度な機能

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class AdvancedMSTestFeatures
{
    // データ駆動テスト
    [TestMethod]
    [DataRow("apple", 5)]
    [DataRow("banana", 6)]
    [DataRow("cherry", 6)]
    public void TestStringLength(string input, int expectedLength)
    {
        Assert.AreEqual(expectedLength, input.Length);
    }
    
    // 動的データプロバイダー
    [TestMethod]
    [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)]
    public void TestWithDynamicData(int value, int expected)
    {
        var result = Math.Abs(value);
        Assert.AreEqual(expected, result);
    }
    
    public static IEnumerable<object[]> GetTestData()
    {
        yield return new object[] { -5, 5 };
        yield return new object[] { 10, 10 };
        yield return new object[] { -15, 15 };
    }
    
    // 条件付きテスト
    [TestMethod]
    public void TestOnlyOnWindows()
    {
        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            Assert.Inconclusive("このテストはWindowsでのみ実行されます");
        }
        
        // Windows固有のテストロジック
        Assert.IsTrue(true);
    }
    
    // カテゴリ別テスト
    [TestMethod]
    [TestCategory("Integration")]
    [TestCategory("Database")]
    public void DatabaseIntegrationTest()
    {
        // 統合テストのロジック
        Assert.IsTrue(true);
    }
    
    [TestMethod]
    [TestCategory("Unit")]
    public void UnitTest()
    {
        // 単体テストのロジック
        Assert.IsTrue(true);
    }
    
    // カスタム属性
    [TestMethod]
    [Owner("開発チーム")]
    [Priority(1)]
    [Description("重要な機能のテスト")]
    public void CriticalFeatureTest()
    {
        // 重要な機能のテスト
        Assert.IsTrue(true);
    }
    
    // 並列実行テスト
    [TestMethod]
    public void ParallelTest1()
    {
        Thread.Sleep(1000);
        Assert.IsTrue(true);
    }
    
    [TestMethod]
    public void ParallelTest2()
    {
        Thread.Sleep(1000);
        Assert.IsTrue(true);
    }
    
    // アサーションの種類
    [TestMethod]
    public void TestVariousAssertions()
    {
        // 基本アサーション
        Assert.IsTrue(true);
        Assert.IsFalse(false);
        Assert.IsNull(null);
        Assert.IsNotNull("value");
        
        // 文字列アサーション
        StringAssert.Contains("Hello World", "World");
        StringAssert.StartsWith("Hello World", "Hello");
        StringAssert.EndsWith("Hello World", "World");
        
        // コレクションアサーション
        var list = new List<int> { 1, 2, 3 };
        CollectionAssert.Contains(list, 2);
        CollectionAssert.DoesNotContain(list, 5);
        
        // 例外アサーション
        Assert.ThrowsException<ArgumentException>(() => {
            throw new ArgumentException("test");
        });
    }
}