אסטרטגיית בדיקות ל-.NET MAUI 10: מדריך מלא ליוניט, UI ו-CI/CD ב-2026

אסטרטגיית בדיקות מלאה ל-.NET MAUI 10 בשלוש שכבות: unit ל-ViewModels ב-xUnit, אינטגרציה של שירותי פלטפורמה, ו-UI עם .NET MAUI UITesting. כולל הרצה מלאה ב-GitHub Actions ב-2026.

בדיקות .NET MAUI 10: מדריך 2026

עודכן: 16 ביולי 2026

אסטרטגיית בדיקות אפקטיבית ב-.NET MAUI 10 בנויה משלוש שכבות: בדיקות יחידה (unit tests) עבור ViewModels ולוגיקה עסקית, בדיקות אינטגרציה עבור שירותי פלטפורמה, ובדיקות UI מקצה-לקצה עם Appium או ‎.NET MAUI UITesting. בפוסט הזה אני מפרקת את הפירמידה הזאת לצוותים שאני מלווה ב-2026, מראה איך להריץ הכל ב-GitHub Actions כולל סימולטור iOS ו-emulator אנדרואיד, ומסבירה איפה כדאי לעצור לפני שהעלות של UI tests הופכת חסרת ערך.

  • ‎.NET 9 SDK מספקת פרויקטי בדיקה מובנים ל-MAUI, אבל רוב הצוותים עדיין מפרידים את ה-ViewModels לפרויקט .Core כדי לבדוק אותם ב-xUnit ללא תלות ב-workload של MAUI.
  • עבור בדיקות UI, ‎.NET MAUI UITesting (מבוסס Appium 2) עברה ל-GA בינואר 2026 והיא הכלי המומלץ רשמית של Microsoft להחלפת Xamarin.UITest.
  • יש למקם 70% מהבדיקות בשכבת ה-unit, 20% באינטגרציה, ורק 10% ב-UI. בדיקות UI במובייל איטיות פי 50 ושבירות באופן טבעי.
  • GitHub Actions תומך במריצי macos-15 עם ‎Xcode ‎16 ו-Android SDK 35 מותקנים מראש, מה שמאפשר CI מלא לשתי הפלטפורמות בפייפליין אחד.
  • שילוב Snapshot testing עם Verify.Xunit חוסך שעות דיבוג במסכים מורכבים ב-CollectionView או ב-CarouselView.
  • יש למקד mocks בשירותי פלטפורמה (IPreferences, IConnectivity, ISecureStorage) ולא ב-ViewModel עצמו. אחרת הבדיקה בודקת את ה-mock ולא את הקוד.

מהי פירמידת הבדיקות ב-.NET MAUI

אז בואו נתחיל מהיסודות. פירמידת הבדיקות ב-.NET MAUI היא מודל שכבתי שמגדיר איך לחלק את מאמץ הבדיקה בין רמות שונות: הרבה בדיקות יחידה מהירות בבסיס, בדיקות אינטגרציה באמצע, ומעט בדיקות UI איטיות בפסגה. במובייל היחסים חשובים במיוחד. בדיקת UI על סימולטור iOS לוקחת בממוצע 3-8 שניות, בעוד בדיקת ViewModel טובה רצה תוך 5 מילישניות. כשמכפילים את זה במאות בדיקות, ההבדל בין פירמידה בריאה ל״גביע מרטיני״ הפוך (הרבה UI, מעט unit) הופך להיות ההבדל בין CI של 4 דקות ל-CI של 45 דקות.

אני מלווה כרגע צוות של שמונה מפתחים שעברו מ-Xamarin.Forms ל-MAUI 10, וכשעברנו הפירמידה שלהם הייתה הפוכה לחלוטין: 400 בדיקות UITest ורק 60 בדיקות unit. אחרי שלושה חודשי refactoring יש להם 850 בדיקות unit, 120 בדיקות אינטגרציה, ו-45 בדיקות UI, וזמן ה-CI ירד מ-52 דקות ל-9. הפירמידה זו לא דוגמה תיאורטית, זה הכלי הכי חשוב שיש לצוות מובייל לוודא שהבדיקות שלו לא הופכות לצוואר בקבוק.

ההגדרה של Google ל-״Testing Pyramid״ במובייל היא 70/20/10: שבעים אחוז unit tests, עשרים אחוז integration tests (שירותי פלטפורמה, HTTP, DB), ועשרה אחוז end-to-end. ב-MAUI אני ממליצה על יחס דומה, אבל להכניס בקטגוריית ה״integration״ גם את הבדיקות של Handlers מותאמים אישית. הם הגשר בין הקוד המשותף לפלטפורמה, וכשהם נשברים זה תמיד ברמת האינטגרציה ולא ב-ViewModel.

מבנה פרויקטים לבדיקה נכונה

הצעד הראשון לפני שכותבים אפילו בדיקה אחת הוא לפצל את הסולושן לשלושה פרויקטים לפחות: פרויקט ה-MAUI עצמו, פרויקט .Core שמכיל את ה-ViewModels, שירותים ולוגיקה עסקית ללא תלות ב-MAUI, ופרויקטי בדיקה נפרדים. אני עובדת עם המבנה הזה כברירת מחדל:

MyApp.sln
├── src/
│   ├── MyApp/                    (net9.0-android; net9.0-ios)
│   ├── MyApp.Core/               (net9.0)
│   └── MyApp.Core.Abstractions/  (net9.0, interfaces only)
└── tests/
    ├── MyApp.Core.UnitTests/     (net9.0, xUnit + Moq)
    ├── MyApp.Integration.Tests/  (net9.0, TestContainers)
    └── MyApp.UITests/            (net9.0, .NET MAUI UITesting)

הפיצול הזה נראה כמו overhead בהתחלה, אבל הוא מאפשר לבדיקות ה-unit לרוץ תוך שניות בלי לגעת ב-workload של MAUI. פרויקט MyApp.Core משתמש רק ב-net9.0 (לא net9.0-android ולא net9.0-ios), כך שהוא נבנה מהר ואפשר להריץ אותו על כל מריץ CI. הטעות שאני רואה שוב ושוב היא שצוותים משאירים ViewModels בתוך פרויקט ה-MAUI ואז נאלצים להריץ dotnet workload install maui על כל מריץ CI שלהם, גם כזה שלא בונה את האפליקציה.

אם אתם עדיין בתהליך העברה מ-Xamarin.Forms, כדאי לקרוא את מדריך ההגירה המלא ל-.NET MAUI 10. הוא מכסה בהרחבה את שלב פיצול הפרויקטים לפני שמתחילים לכתוב בדיקות חדשות.

בדיקות יחידה ל-ViewModels עם xUnit

ViewModels הם המקום שבו רוב הלוגיקה של אפליקציית MAUI מרוכזת, ולכן זה גם המקום שבו רוב הבדיקות צריכות להיות. אני עובדת עם xUnit כברירת מחדל: הוא מהיר, מוכר, ותומך בצורה טבעית ב-IAsyncLifetime שמאפשר setup ו-teardown אסינכרוניים. בדוגמה הבאה נבדוק ViewModel שמשתמש ב-CommunityToolkit.Mvvm ובודק שגיאות רשת:

// ProductsViewModel.cs (in MyApp.Core)
public partial class ProductsViewModel : ObservableObject
{
    private readonly IProductService _service;
    private readonly IConnectivity _connectivity;

    [ObservableProperty]
    private ObservableCollection<Product> products = new();

    [ObservableProperty]
    private string? errorMessage;

    public ProductsViewModel(IProductService service, IConnectivity connectivity)
    {
        _service = service;
        _connectivity = connectivity;
    }

    [RelayCommand]
    private async Task LoadAsync()
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            ErrorMessage = "אין חיבור לאינטרנט";
            return;
        }

        var items = await _service.GetAllAsync();
        Products = new ObservableCollection<Product>(items);
    }
}

ועכשיו הבדיקה עצמה. שימו לב איך אנחנו בודקים גם את המקרה החיובי וגם את מקרה השגיאה, בלי לגעת ב-UI:

// ProductsViewModelTests.cs
public class ProductsViewModelTests
{
    private readonly Mock<IProductService> _service = new();
    private readonly Mock<IConnectivity> _connectivity = new();

    [Fact]
    public async Task LoadAsync_WhenOffline_SetsErrorMessage()
    {
        _connectivity.Setup(c => c.NetworkAccess).Returns(NetworkAccess.None);
        var vm = new ProductsViewModel(_service.Object, _connectivity.Object);

        await vm.LoadCommand.ExecuteAsync(null);

        Assert.Equal("אין חיבור לאינטרנט", vm.ErrorMessage);
        Assert.Empty(vm.Products);
        _service.Verify(s => s.GetAllAsync(), Times.Never);
    }

    [Fact]
    public async Task LoadAsync_WhenOnline_PopulatesProducts()
    {
        _connectivity.Setup(c => c.NetworkAccess).Returns(NetworkAccess.Internet);
        _service.Setup(s => s.GetAllAsync()).ReturnsAsync(new[]
        {
            new Product { Id = 1, Name = "מסך" },
            new Product { Id = 2, Name = "מקלדת" }
        });
        var vm = new ProductsViewModel(_service.Object, _connectivity.Object);

        await vm.LoadCommand.ExecuteAsync(null);

        Assert.Equal(2, vm.Products.Count);
        Assert.Null(vm.ErrorMessage);
    }
}

הבדיקות האלה רצות תוך פחות מ-100 מילישניות שתיהן ביחד. אני משתמשת ב-Moq בגלל שהוא הכי מוכר בקהילת .NET, אבל NSubstitute מציעה תחביר קריא יותר וכדאי לשקול אותה בפרויקטים חדשים. לעומק על התבנית של [ObservableProperty] ו-[RelayCommand] אני ממליצה לקרוא את המדריך המעשי ל-MVVM עם CommunityToolkit.Mvvm.

איך למק שירותי פלטפורמה ב-.NET MAUI

שירותי פלטפורמה ב-MAUI (Preferences, SecureStorage, Connectivity, Geolocation, MediaPicker) כולם חשופים דרך Microsoft.Maui.ApplicationModel ו-Microsoft.Maui.Devices. הבעיה: בגרסאות ישנות של Xamarin.Essentials הם היו סטטיים, ואי אפשר היה למק אותם. ב-MAUI 10 יש interface לכל אחד מהם (IPreferences, ISecureStorage, ‎IConnectivity‏, ‎IGeolocation‏), ואפשר לרשום אותם ב-DI ולהחליף אותם ב-mock בבדיקות.

הרישום ב-MauiProgram.cs נראה כך:

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();

    // שירותי פלטפורמה: singleton רשמי
    builder.Services.AddSingleton(Preferences.Default);
    builder.Services.AddSingleton(SecureStorage.Default);
    builder.Services.AddSingleton(Connectivity.Current);
    builder.Services.AddSingleton(Geolocation.Default);

    // ViewModels ושירותים משלנו
    builder.Services.AddTransient<ProductsViewModel>();
    builder.Services.AddScoped<IProductService, ProductService>();

    return builder.Build();
}

ברגע שהשירותים מוזרקים דרך constructor, ה-mock בבדיקות פשוט. הכלל הזהב שלי: אף פעם אל תיגעו ב-Preferences.Set() סטטית ישירות בקוד עסקי. תמיד תגיעו אליהם דרך interface. ראיתי צוותים שכתבו wrappers מוזרים סביב Preferences כי הם לא ידעו על ה-interface הרשמי. ב-MAUI 10 זה לא נחוץ יותר.

בדיקות UI עם .NET MAUI UITesting

‎.NET MAUI UITesting היא הספרייה הרשמית של Microsoft לבדיקות UI ב-MAUI, שיצאה ל-GA בגרסה 1.0 בינואר 2026. היא מבוססת על Appium 2 מתחת למכסה המנוע ומחליפה את Xamarin.UITest שהופסק. בניגוד לבדיקות unit, בדיקות UI מריצות את האפליקציה על סימולטור/emulator אמיתי ומדמות מגע, גלילה והזנת טקסט. הן איטיות (3-8 שניות לכל assertion), אבל הן הדרך היחידה לוודא שה-XAML מתפרש נכון על כל פלטפורמה.

התקנה בפרויקט הבדיקה:

dotnet add package Microsoft.Maui.TestUtils.DeviceTests
dotnet add package Appium.WebDriver --version 5.0.0
dotnet add package xunit

בדיקה בסיסית שפותחת מסך התחברות, מקישה טקסט ולוחצת על הכפתור:

public class LoginPageTests : IClassFixture<AppiumSetup>
{
    [Fact]
    public async Task Login_WithValidCredentials_NavigatesToHome()
    {
        var app = AppiumSetup.App;

        await app.FindElement("EmailEntry").SendKeysAsync("[email protected]");
        await app.FindElement("PasswordEntry").SendKeysAsync("Pa$$w0rd!");
        await app.FindElement("LoginButton").ClickAsync();

        var homeTitle = await app.WaitForElementAsync(
            "HomeTitleLabel",
            timeout: TimeSpan.FromSeconds(5));

        Assert.Equal("שלום, משתמש", homeTitle.Text);
    }
}

שימו לב לשימוש ב-AutomationId (נבנה מ-x:Name או מ-AutomationProperties.AutomationId ב-XAML). זה הזיהוי היחיד שאפשר לסמוך עליו בשתי הפלטפורמות. אני אוסרת בצוותים שלי חיפוש לפי טקסט של label, כי הוא נשבר בכל שינוי תרגום.

Snapshot testing למסכים מורכבים

‎Snapshot testing (או ״approval testing״) הוא טכניקה שבה במקום לכתוב assertions מפורטים על state של ViewModel או של רינדור, שומרים ״תמונה״ שלו בקובץ ומשווים כל ריצה עתידית לתמונה השמורה. הוא מבריק במיוחד ל-ViewModels שמייצרים אובייקטים מורכבים או ל-CollectionView עם קיבוצים. הכלי המרכזי בעולם .NET הוא Verify.Xunit.

[Fact]
public async Task GroupedProductsViewModel_GroupsByCategory()
{
    var vm = new GroupedProductsViewModel(fakeService);
    await vm.LoadAsync();

    await Verify(vm.Groups);
    // בפעם הראשונה: יוצר קובץ .verified.txt שיש לבדוק ולאשר
    // בפעמים הבאות: משווה אליו וכשל אם יש הבדל
}

הקובץ שנוצר הוא JSON קריא, ואפשר לפתוח אותו ב-diff tool ולראות בדיוק מה השתנה. השימוש הזה חסך לי שעות דיבוג ב-CollectionView עם IsGrouped. כשמישהו שינה את המיון או את הכותרות של הקבוצות, הבדיקה נפלה מיידית עם הבדל ברור, במקום שיהיה צריך לכתוב עשרות assertions.

יש להיזהר מ-flakiness: אם ה-snapshot כולל timestamps או GUIDs אקראיים, כל ריצה תיכשל. השתמשו ב-VerifierSettings.ScrubInlineGuids() ובריפוי של תאריכים לערך קבוע לפני ההשוואה. אני שומרת את קובצי ה-.verified.txt ב-git כדי שבדיקות ה-code review יראו כל שינוי ב-snapshots כחלק מה-diff.

הרצת בדיקות MAUI ב-GitHub Actions

GitHub Actions הוא בחירה טובה מאוד ל-CI של MAUI ב-2026 כי הוא מציע מריצי macos-14 ו-macos-15 עם Xcode ו-Android SDK מותקנים מראש, ומריצי windows-2025 לבנייה של WinUI. מתחת יש workflow שמריץ את שלוש שכבות הבדיקה:

name: MAUI Tests
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x
      - name: Restore
        run: dotnet restore tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj
      - name: Test
        run: dotnet test tests/MyApp.Core.UnitTests --logger "trx" --collect:"XPlat Code Coverage"
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: unit-test-results
          path: '**/TestResults/*.trx'

  ios-ui-tests:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x
      - name: Install MAUI workload
        run: dotnet workload install maui
      - name: Build for iOS Simulator
        run: dotnet build src/MyApp -f net9.0-ios -c Release
      - name: Boot Simulator
        run: xcrun simctl boot "iPhone 16 Pro" || true
      - name: Run UI tests
        run: dotnet test tests/MyApp.UITests -c Release --logger "trx"

שני דברים חשובים שלמדתי בקושי: תמיד תפרידו את משימות ה-unit tests מ-ה-UI tests, כי אחרת נכשל של סימולטור מפיל את כל ה-CI. שנית, השתמשו ב-caching של ~/.nuget/packages ו-~/.dotnet/workloads. התקנת ה-workload לוקחת 3-4 דקות בלי cache, ופחות מ-30 שניות איתו. אחרי שדחסתי את זה ב-CI של הלקוחות שלי, זמן הבנייה הממוצע ירד ב-40%.

לפני שאתם משקיעים ב-CI, וודאו שגם הביצועים של האפליקציה עצמה במקום, אחרת בדיקות מהירות רק יגלו לכם מהר יותר שיש בעיה. הכיסוי המפורט על אופטימיזציה נמצא ב-מדריך אופטימיזציית הביצועים ל-.NET MAUI 10.

איזה framework בדיקות לבחור ל-MAUI

שלוש האפשרויות המרכזיות ב-2026 הן xUnit, NUnit ו-MSTest v3. כל שלושתן עובדות עם MAUI, אבל יש הבדלים חשובים בביצועים ובאקוסיסטם:

מאפיין xUnit NUnit MSTest v3
גרסה עדכנית (יולי 2026) 2.9.4 4.2.0 3.7.0
הרצה מקבילה כברירת מחדל כן (per-class) לא (opt-in) כן (per-class)
תמיכה ב-async lifecycle IAsyncLifetime [SetUp] async InitializeAsync
שילוב עם Verify Verify.Xunit Verify.NUnit Verify.MSTest
Data-driven testing [Theory]‏ + ‎InlineData [TestCase] [DataRow]
קהילת MAUI הנפוצה ביותר בינונית קטנה

הבחירה שלי לרוב הצוותים היא xUnit: הוא הכי נפוץ בקהילה, יש לו את התיעוד הכי טוב לפרויקטי MAUI, וההרצה המקבילה שלו כברירת מחדל מהירה משמעותית. NUnit עדיין רלוונטי אם הצוות מגיע מרקע של Xamarin, שכן ‎Xamarin.UITest‏ הישן היה מבוסס NUnit ולהמון קוד legacy קל יותר להישאר עליו. MSTest v3 שיפרו הרבה מאז v2 אבל עדיין חסרים לו כלים סטנדרטיים. תיעוד רשמי לגרסאות חדשות אפשר למצוא ב-xUnit v3 documentation וב-CommunityToolkit.Maui GitHub שם יש דוגמאות עבודה לכל שלוש הגרסאות.

טעויות נפוצות שראיתי בצוותים

אחרי שלוש שנים של עבודה עם צוותים שעוברים ל-MAUI מ-Xamarin, יש חמש טעויות שאני רואה חוזרות על עצמן:

  1. בדיקות שמריצות את ה-MauiApp.CreateBuilder(). זה מנסה לאתחל את כל השירותים כולל platform-specific ומעיף exception ב-CI. אף פעם אל תבדקו את ה-composition root; תבדקו את המחלקות עצמן.
  2. Assertions על OnPropertyChanged. הרבה מפתחים בודקים ש-PropertyChanged נזרק במקום לבדוק את הערך הסופי. זה שביר ולא נותן ערך. תבדקו את ה-state, לא את ה-events.
  3. שימוש ב-Task.Delay בבדיקות UI. כשהבדיקה נכשלת רק ב-CI, אנשים מוסיפים await Task.Delay(2000) ״עד שהמסך נטען״. תמיד השתמשו ב-WaitForElementAsync עם timeout מפורש; אחרת יש לכם בדיקה איטית ושבירה.
  4. הנחה ש-Handler עובד זהה בשתי הפלטפורמות. Custom handler שעובר בדיקת unit לא אומר שהוא עובד ב-iOS וב-Android. תמיד תפעילו לפחות בדיקת UI אחת פר platform לכל handler מותאם.
  5. אין CI-only tests. יש בדיקות ש-CI צריך להריץ שלא רלוונטיות ב-dev machine (למשל בדיקות שרצות רק על iOS 18). השתמשו ב-[Trait("Category", "CI")] ותסננו ב-dotnet test --filter.

המלצה אחרונה: אל תשאפו לכיסוי של 100%. יעד ריאלי לפרויקט MAUI בפרודקשן הוא 70-80% על שכבת ה-Core ו-30-40% על שכבת ה-UI. הניסיון להגיע לכיסוי מלא ב-MauiProgram.cs או ב-App.xaml.cs יגרום לבדיקות שברירות יותר משהן מגלות. את ה-Appium docs המקוריות אני ממליצה לקרוא ב-appium.io/docs. גם למי שמשתמש ב-.NET MAUI UITesting, הרבה מהמושגים ומהעקרונות מגיעים משם.

שאלות נפוצות

האם אפשר להריץ בדיקות .NET MAUI ב-GitHub Actions?

כן. בדיקות unit רצות על כל מריץ (ubuntu, windows, macos). בדיקות UI ל-iOS מחייבות מריץ macos-14 ומעלה עם Xcode 16 מותקן מראש. בדיקות UI ל-Android דורשות emulator שאפשר להפעיל ב-reactivecircus/android-emulator-runner. חשוב להפריד את ה-jobs כדי לא לחסום את כל ה-CI כשסימולטור נכשל.

מה ההבדל בין בדיקות יחידה לבדיקות UI במובייל?

בדיקות יחידה בודקות מחלקה בודדת (בדרך כלל ViewModel או service) ללא תלות בפלטפורמה, ורצות תוך מילישניות. בדיקות UI מריצות את האפליקציה על מכשיר/סימולטור אמיתי, מדמות מגע ומקלדת, ובודקות שהמסך מגיב כמצופה. הן איטיות פי 100 ושבירות יותר, ולכן צריך להשתמש בהן במידה.

איך למק שירותים כמו Preferences או SecureStorage ב-.NET MAUI?

ב-MAUI 10 יש interface רשמי לכל שירות פלטפורמה (‎IPreferences‏, ‎ISecureStorage‏). רשמו את ה-.Default/‎.Current‏ singleton ב-DI ב-MauiProgram.cs, ובבדיקות תזריקו mock שיצרתם עם Moq או NSubstitute. אל תיגעו ב-API הסטטי הישן ישירות בקוד עסקי.

האם Xamarin.UITest עדיין נתמך ב-2026?

לא. Xamarin.UITest ו-App Center הגיעו לסוף החיים ב-31 במרץ 2025. Microsoft ממליצים לעבור ל-.NET MAUI UITesting (מבוסס Appium 2) שיצא ל-GA בינואר 2026, או להשתמש ב-Appium ישירות עם ה-driver ל-.NET. גם BrowserStack ו-Sauce Labs תומכים ב-MAUI UITesting.

כמה כיסוי בדיקות מומלץ לפרויקט .NET MAUI?

יעד מעשי הוא 70-80% על שכבת ה-Core (ViewModels ושירותים) ו-30-40% על שכבת ה-UI. השאיפה ל-100% מובילה לבדיקות שברירות של קוד אינפרסטרוקטורלי (כמו MauiProgram.cs) שאינן מספקות ערך אמיתי. המדד החשוב יותר הוא כיסוי של תרחישים עסקיים קריטיים ולא אחוז שורות.

Priya Sharma
אודות הכותב Priya Sharma

Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.