למה בדיקות הן לא מותרות — הן חובה
אם עקבתם אחרי המדריך הקודם שלנו על בניית אפליקציית MAUI עם MVVM, בטח שמתם לב שהשקענו אנרגיה לא מעטה בארכיטקטורה נקייה — הפרדה בין שכבות, הזרקת תלויות, ו-ViewModels עם CommunityToolkit.Mvvm. כל זה נהדר, אבל בלי בדיקות? זה קצת כמו לבנות בית מפואר בלי ביטוח. הכל עובד מעולה — עד שזה לא.
בואו נהיה כנים לרגע. רוב המפתחים שאני מכיר לא אוהבים לכתוב בדיקות. זה מרגיש כמו "עבודה כפולה". אבל אחרי שבאג קריטי מגיע לפרודקשן ביום שישי בצהריים — פתאום בדיקות נשמעות כמו רעיון מצוין.
ב-.NET MAUI 10, מיקרוסופט שמה דגש חזק על איכות קוד. המערכת החדשה של Diagnostics ו-Metrics (עם ActivitySource ו-Microsoft.Maui Meters) נותנת לנו כלים מובנים לעקוב אחרי ביצועים. אבל הכלי הכי חשוב לאיכות קוד הוא ותמיד יהיה — בדיקות אוטומטיות.
במדריך הזה נבנה תשתית בדיקות מלאה לאפליקציית .NET MAUI 10. נתחיל מבדיקות יחידה ל-ViewModels, נעבור לבדיקות שירותים עם מוקים, ונסיים עם בדיקות אינטגרציה ו-UI. כל דוגמת קוד כאן עובדת — אפשר פשוט להעתיק, להדביק ולהריץ.
הגדרת פרויקט הבדיקות
יצירת פרויקט xUnit
xUnit היא ספריית הבדיקות הפופולרית ביותר בעולם .NET, וזה לא מקרי. היא מודרנית, מהירה, ומשתלבת מצוין עם כלי ה-CI/CD. אז בואו נתחיל ביצירת פרויקט בדיקות:
dotnet new xunit -n TaskMaster.Tests
cd TaskMaster.Tests
dotnet add reference ../TaskMaster/TaskMaster.csproj
עכשיו נתקין את כל החבילות שנצטרך:
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package CommunityToolkit.Mvvm
dotnet add package sqlite-net-pcl
Moq היא ספריית Mocking שמאפשרת לנו ליצור אובייקטים מזויפים במקום השירותים האמיתיים. FluentAssertions הופכת את האסרציות לקריאות הרבה יותר — במקום Assert.Equal נכתוב result.Should().Be(expected). בכנות, אחרי שמתרגלים ל-FluentAssertions קשה לחזור אחורה.
מבנה תיקיות מומלץ
כמו שמבנה טוב חשוב בפרויקט הראשי, כך גם בפרויקט הבדיקות. הנה המבנה שאני ממליץ עליו:
TaskMaster.Tests/
├── ViewModels/
│ ├── TaskListViewModelTests.cs
│ ├── TaskDetailViewModelTests.cs
│ └── AddTaskViewModelTests.cs
├── Services/
│ ├── TaskServiceTests.cs
│ └── DatabaseServiceTests.cs
├── Integration/
│ ├── NavigationTests.cs
│ └── DependencyInjectionTests.cs
├── Helpers/
│ ├── MockFactory.cs
│ └── TestDataBuilder.cs
└── GlobalUsings.cs
הקובץ GlobalUsings.cs חוסך הרבה שורות חוזרות בכל קובץ בדיקה. טיפ קטן שעושה הבדל גדול:
// GlobalUsings.cs
global using Xunit;
global using Moq;
global using FluentAssertions;
global using TaskMaster.Models;
global using TaskMaster.Services;
global using TaskMaster.ViewModels;
בדיקות יחידה ל-ViewModels
ה-ViewModels הם הלב של האפליקציה. הם מכילים את הלוגיקה העסקית, מנהלים את מצב הממשק, ומטפלים באינטראקציות של המשתמש. אם הם עובדים נכון — רוב האפליקציה עובדת נכון.
בדיקת טעינת משימות
נתחיל עם בדיקה בסיסית: האם ה-ViewModel טוען משימות בהצלחה? שימו לב איך אנחנו משתמשים ב-Mock של ITaskService כדי לבודד את הבדיקה לחלוטין מהמסד נתונים:
// ViewModels/TaskListViewModelTests.cs
namespace TaskMaster.Tests.ViewModels;
public class TaskListViewModelTests
{
private readonly Mock<ITaskService> _mockTaskService;
private readonly TaskListViewModel _viewModel;
public TaskListViewModelTests()
{
_mockTaskService = new Mock<ITaskService>();
_viewModel = new TaskListViewModel(_mockTaskService.Object);
}
[Fact]
public async Task LoadTasks_ShouldPopulateTasksList_WhenServiceReturnsData()
{
// Arrange
var expectedTasks = new List<TaskItem>
{
new() { Id = 1, Title = "משימה ראשונה", IsCompleted = false },
new() { Id = 2, Title = "משימה שנייה", IsCompleted = true },
new() { Id = 3, Title = "משימה שלישית", IsCompleted = false }
};
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(expectedTasks);
// Act
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
_viewModel.Tasks.Should().HaveCount(3);
_viewModel.Tasks.First().Title.Should().Be("משימה ראשונה");
_viewModel.IsEmpty.Should().BeFalse();
_viewModel.IsLoading.Should().BeFalse();
}
[Fact]
public async Task LoadTasks_ShouldSetIsEmpty_WhenNoTasksExist()
{
// Arrange
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(new List<TaskItem>());
// Act
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
_viewModel.Tasks.Should().BeEmpty();
_viewModel.IsEmpty.Should().BeTrue();
}
}
שימו לב לדפוס Arrange-Act-Assert (או בקיצור AAA). כל בדיקה מחולקת לשלושה חלקים ברורים: הכנה, פעולה, ובדיקת תוצאה. זה הופך את הבדיקות לקריאות וקלות לתחזוקה — מה שנשמע כמו פרט קטן, אבל עושה הבדל ענק כשיש לכם 200 בדיקות בפרויקט.
בדיקת פקודות (RelayCommand)
אחד הדברים הטובים ב-CommunityToolkit.Mvvm הוא שהפקודות שנוצרות עם [RelayCommand] הן ישויות שאפשר להפעיל ולבדוק ישירות. נבדוק את פעולת המחיקה:
[Fact]
public async Task DeleteTask_ShouldRemoveFromList_WhenConfirmed()
{
// Arrange
var taskToDelete = new TaskItem { Id = 1, Title = "משימה למחיקה" };
var tasks = new List<TaskItem> { taskToDelete };
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(tasks);
_mockTaskService
.Setup(s => s.DeleteTaskAsync(1))
.Returns(Task.CompletedTask);
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
_viewModel.Tasks.Should().HaveCount(1);
// Act
await _viewModel.DeleteTaskCommand.ExecuteAsync(taskToDelete);
// Assert
_mockTaskService.Verify(s => s.DeleteTaskAsync(1), Times.Once);
}
[Fact]
public async Task DeleteTask_ShouldUpdateIsEmpty_WhenLastTaskDeleted()
{
// Arrange
var lastTask = new TaskItem { Id = 1, Title = "משימה אחרונה" };
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(new List<TaskItem> { lastTask });
_mockTaskService
.Setup(s => s.DeleteTaskAsync(1))
.Returns(Task.CompletedTask);
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Act
await _viewModel.DeleteTaskCommand.ExecuteAsync(lastTask);
// Assert
_viewModel.Tasks.Should().BeEmpty();
_viewModel.IsEmpty.Should().BeTrue();
}
בדיקת סינון וחיפוש
סינון הוא לוגיקה עסקית שממש חשוב לכסות בבדיקות. נבדוק שהחיפוש עובד כמו שצריך:
[Theory]
[InlineData("קניות", 1)]
[InlineData("משימה", 3)]
[InlineData("לא קיים", 0)]
[InlineData("", 3)]
public async Task FilterTasks_ShouldReturnMatchingTasks(
string searchText, int expectedCount)
{
// Arrange
var allTasks = new List<TaskItem>
{
new() { Id = 1, Title = "קניות בסופר", Description = "חלב, לחם, ביצים" },
new() { Id = 2, Title = "משימה בעבודה", Description = "לסיים דוח" },
new() { Id = 3, Title = "משימה ביתית", Description = "לנקות משימה" }
};
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(allTasks);
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Act
_viewModel.SearchText = searchText;
// המתנה קצרה לסינון אסינכרוני
await Task.Delay(100);
// Assert
_viewModel.Tasks.Should().HaveCount(expectedCount);
}
שימו לב לשימוש ב-[Theory] עם [InlineData] — זה מאפשר לנו להריץ את אותה בדיקה עם ערכים שונים בלי לשכפל קוד. ב-xUnit, [Fact] הוא בדיקה בודדת ו-[Theory] הוא בדיקה פרמטרית. פיצ'ר קטן ושימושי מאוד.
בדיקת שינויי מצב (INotifyPropertyChanged)
אחד הדברים הקריטיים ב-MVVM הוא שהממשק מתעדכן כשהמצב משתנה. בואו נוודא שהאירועים באמת נשלחים כשצריך:
[Fact]
public async Task LoadTasks_ShouldNotifyPropertyChanged_ForIsLoading()
{
// Arrange
var propertyChanges = new List<string>();
_viewModel.PropertyChanged += (_, e) =>
propertyChanges.Add(e.PropertyName!);
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(new List<TaskItem>());
// Act
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
propertyChanges.Should().Contain("IsLoading");
propertyChanges.Should().Contain("Tasks");
propertyChanges.Should().Contain("IsEmpty");
}
[Fact]
public void SearchText_ShouldNotifyPropertyChanged_WhenSet()
{
// Arrange
var raised = false;
_viewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == "SearchText") raised = true;
};
// Act
_viewModel.SearchText = "חיפוש חדש";
// Assert
raised.Should().BeTrue();
_viewModel.SearchText.Should().Be("חיפוש חדש");
}
Mocking שירותים עם Moq
Mocking הוא הטכניקה שמאפשרת לנו לבדוק רכיב אחד בבידוד מוחלט. במקום להתחבר למסד נתונים אמיתי או לשלוח בקשות רשת — אנחנו יוצרים "שירות מזויף" שמחזיר בדיוק מה שאנחנו רוצים.
אגב, זה גם מה שהופך את הבדיקות למהירות ברמות. אין IO, אין רשת, אין דיסק. הכל בזיכרון.
הגדרת Mock מתקדם
לפעמים צריך לבדוק תרחישים יותר מורכבים. הנה דוגמה למוק שמדמה שירות עם מצב פנימי (וזה כבר ממש שימושי בפרויקטים אמיתיים):
public class TaskServiceMockFactory
{
public static Mock<ITaskService> CreateWithTasks(
params TaskItem[] tasks)
{
var mock = new Mock<ITaskService>();
var taskList = new List<TaskItem>(tasks);
mock.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(() => taskList.ToList());
mock.Setup(s => s.GetTaskByIdAsync(It.IsAny<int>()))
.ReturnsAsync((int id) =>
taskList.FirstOrDefault(t => t.Id == id));
mock.Setup(s => s.AddTaskAsync(It.IsAny<TaskItem>()))
.Callback<TaskItem>(task =>
{
task.Id = taskList.Count + 1;
taskList.Add(task);
})
.Returns(Task.CompletedTask);
mock.Setup(s => s.DeleteTaskAsync(It.IsAny<int>()))
.Callback<int>(id =>
taskList.RemoveAll(t => t.Id == id))
.Returns(Task.CompletedTask);
return mock;
}
}
ואז השימוש הוא פשוט:
[Fact]
public async Task AddTask_ShouldAppearInList_AfterRefresh()
{
// Arrange
var mock = TaskServiceMockFactory.CreateWithTasks(
new TaskItem { Id = 1, Title = "משימה קיימת" }
);
var viewModel = new TaskListViewModel(mock.Object);
// Act - הוספת משימה
await mock.Object.AddTaskAsync(
new TaskItem { Title = "משימה חדשה" });
await viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
viewModel.Tasks.Should().HaveCount(2);
viewModel.Tasks.Last().Title.Should().Be("משימה חדשה");
}
בדיקת תרחישי שגיאה
וזה החלק שהרבה מפתחים מדלגים עליו (ואז מתחרטים). אל תשכחו לבדוק מה קורה כשדברים לא עובדים:
[Fact]
public async Task LoadTasks_ShouldHandleServiceException_Gracefully()
{
// Arrange
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ThrowsAsync(new InvalidOperationException("Database connection failed"));
// Act
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
_viewModel.IsLoading.Should().BeFalse();
_viewModel.Tasks.Should().BeEmpty();
}
[Fact]
public async Task LoadTasks_ShouldNotLoadConcurrently()
{
// Arrange
var tcs = new TaskCompletionSource<List<TaskItem>>();
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.Returns(tcs.Task);
// Act - קריאה ראשונה (ממתינה)
var firstCall = _viewModel.LoadTasksCommand.ExecuteAsync(null);
_viewModel.IsLoading.Should().BeTrue();
// קריאה שנייה בזמן שהראשונה עדיין רצה
var secondCall = _viewModel.LoadTasksCommand.ExecuteAsync(null);
// שחרור הקריאה הראשונה
tcs.SetResult(new List<TaskItem>());
await firstCall;
// Assert - הפונקציה נקראה רק פעם אחת
_mockTaskService.Verify(
s => s.GetAllTasksAsync(), Times.Once);
}
בדיקות שכבת Services
בזמן שהבדיקות של ViewModels משתמשות במוקים, בבדיקות של Services אנחנו רוצים לבדוק את הלוגיקה האמיתית. עבור שירותים שמשתמשים ב-SQLite, הטריק הוא להשתמש במסד נתונים בזיכרון — מהיר, נקי, ולא משאיר שאריות:
public class TaskServiceTests : IAsyncLifetime
{
private SQLiteAsyncConnection _connection = null!;
private TaskService _service = null!;
public async Task InitializeAsync()
{
// שימוש ב-SQLite בזיכרון לבדיקות
_connection = new SQLiteAsyncConnection(":memory:");
await _connection.CreateTableAsync<TaskItem>();
_service = new TaskService(_connection);
}
public async Task DisposeAsync()
{
await _connection.CloseAsync();
}
[Fact]
public async Task AddAndRetrieveTask_ShouldWork()
{
// Arrange
var newTask = new TaskItem
{
Title = "בדיקת אינטגרציה",
Description = "לוודא שהכל עובד",
Priority = 2,
DueDate = DateTime.Now.AddDays(7)
};
// Act
await _service.AddTaskAsync(newTask);
var allTasks = await _service.GetAllTasksAsync();
// Assert
allTasks.Should().ContainSingle();
allTasks.First().Title.Should().Be("בדיקת אינטגרציה");
allTasks.First().Id.Should().BeGreaterThan(0);
}
[Fact]
public async Task UpdateTask_ShouldPersistChanges()
{
// Arrange
var task = new TaskItem { Title = "לפני עדכון" };
await _service.AddTaskAsync(task);
// Act
task.Title = "אחרי עדכון";
task.IsCompleted = true;
await _service.UpdateTaskAsync(task);
var updated = await _service.GetTaskByIdAsync(task.Id);
// Assert
updated!.Title.Should().Be("אחרי עדכון");
updated.IsCompleted.Should().BeTrue();
}
[Fact]
public async Task DeleteTask_ShouldRemoveFromDatabase()
{
// Arrange
var task = new TaskItem { Title = "משימה למחיקה" };
await _service.AddTaskAsync(task);
var countBefore = (await _service.GetAllTasksAsync()).Count;
// Act
await _service.DeleteTaskAsync(task.Id);
var countAfter = (await _service.GetAllTasksAsync()).Count;
// Assert
countBefore.Should().Be(1);
countAfter.Should().Be(0);
}
[Fact]
public async Task GetTasksByPriority_ShouldReturnSorted()
{
// Arrange
await _service.AddTaskAsync(
new TaskItem { Title = "נמוכה", Priority = 0 });
await _service.AddTaskAsync(
new TaskItem { Title = "גבוהה", Priority = 2 });
await _service.AddTaskAsync(
new TaskItem { Title = "בינונית", Priority = 1 });
// Act
var tasks = await _service.GetAllTasksAsync();
var sorted = tasks.OrderByDescending(t => t.Priority).ToList();
// Assert
sorted.First().Title.Should().Be("גבוהה");
sorted.Last().Title.Should().Be("נמוכה");
}
}
שימו לב לממשק IAsyncLifetime של xUnit — הוא מאפשר לבצע אתחול ופירוק אסינכרוניים לפני ואחרי כל בדיקה. ככה כל בדיקה מתחילה עם מסד נתונים נקי לגמרי, בלי תלות בסדר הרצה.
בדיקות Dependency Injection
ב-.NET MAUI 10, הזרקת תלויות היא חלק מרכזי מהארכיטקטורה. כדאי מאוד לוודא שכל השירותים רשומים ונפתרים נכון — כי באגים ב-DI הם מהמעצבנים שיש (הם צצים רק בזמן ריצה):
public class DependencyInjectionTests
{
private readonly IServiceProvider _serviceProvider;
public DependencyInjectionTests()
{
var services = new ServiceCollection();
// רישום כמו ב-MauiProgram.cs
services.AddSingleton<ITaskService, TaskService>();
services.AddTransient<TaskListViewModel>();
services.AddTransient<TaskDetailViewModel>();
services.AddTransient<AddTaskViewModel>();
_serviceProvider = services.BuildServiceProvider();
}
[Fact]
public void TaskService_ShouldResolve_AsSingleton()
{
// Act
var service1 = _serviceProvider
.GetRequiredService<ITaskService>();
var service2 = _serviceProvider
.GetRequiredService<ITaskService>();
// Assert - Singleton = אותו אובייקט
service1.Should().BeSameAs(service2);
}
[Fact]
public void ViewModels_ShouldResolve_AsTransient()
{
// Act
var vm1 = _serviceProvider
.GetRequiredService<TaskListViewModel>();
var vm2 = _serviceProvider
.GetRequiredService<TaskListViewModel>();
// Assert - Transient = אובייקטים שונים
vm1.Should().NotBeSameAs(vm2);
}
[Fact]
public void ViewModel_ShouldReceive_InjectedService()
{
// Act
var viewModel = _serviceProvider
.GetRequiredService<TaskListViewModel>();
// Assert - ה-ViewModel התקבל עם Service מוזרק
viewModel.Should().NotBeNull();
}
}
בדיקות UI עם Appium
בדיקות UI מוודאות שהממשק באמת עובד מנקודת המבט של המשתמש. Appium הוא הכלי הסטנדרטי לבדיקות UI חוצות פלטפורמות, והוא עובד מצוין עם .NET MAUI.
נגיד את זה מראש — בדיקות UI הן איטיות יותר ושבריריות יותר מבדיקות יחידה. אל תנסו לכסות הכל עם UI tests. תשתמשו בהן בעיקר לתרחישים קריטיים.
הגדרת Appium
ראשית, התקינו את Appium ואת הדרייבר המתאים:
npm install -g appium
appium driver install uiautomator2 # Android
appium driver install xcuitest # iOS
וצרו פרויקט בדיקות UI נפרד:
dotnet new xunit -n TaskMaster.UITests
cd TaskMaster.UITests
dotnet add package Appium.Net
dotnet add package Appium.WebDriver
כתיבת בדיקת UI בסיסית
public class TaskListPageUITests : IDisposable
{
private readonly AndroidDriver _driver;
public TaskListPageUITests()
{
var options = new AppiumOptions
{
PlatformName = "Android",
AutomationName = "UiAutomator2",
App = "/path/to/com.taskmaster.app.apk"
};
_driver = new AndroidDriver(
new Uri("http://localhost:4723"), options);
_driver.Manage().Timeouts().ImplicitWait =
TimeSpan.FromSeconds(10);
}
[Fact]
public void AddTask_ShouldAppearInList()
{
// Arrange - ניווט לדף הוספת משימה
var addButton = _driver.FindElement(
MobileBy.AccessibilityId("AddTaskButton"));
addButton.Click();
// Act - מילוי הטופס
var titleEntry = _driver.FindElement(
MobileBy.AccessibilityId("TaskTitleEntry"));
titleEntry.SendKeys("משימה מבדיקת UI");
var saveButton = _driver.FindElement(
MobileBy.AccessibilityId("SaveTaskButton"));
saveButton.Click();
// Assert - המשימה מופיעה ברשימה
var taskLabel = _driver.FindElement(
MobileBy.AccessibilityId("TaskItem_משימה מבדיקת UI"));
taskLabel.Should().NotBeNull();
taskLabel.Displayed.Should().BeTrue();
}
[Fact]
public void SwipeToDelete_ShouldRemoveTask()
{
// Arrange
var taskItem = _driver.FindElement(
MobileBy.AccessibilityId("TaskItem_0"));
// Act - החלקה שמאלה למחיקה
var size = taskItem.Size;
var location = taskItem.Location;
var touchAction = new TouchAction(_driver);
touchAction.Press(location.X + size.Width - 10, location.Y + size.Height / 2)
.Wait(200)
.MoveTo(location.X + 10, location.Y + size.Height / 2)
.Release()
.Perform();
// אישור מחיקה
var confirmButton = _driver.FindElement(
MobileBy.AccessibilityId("ConfirmDeleteButton"));
confirmButton.Click();
// Assert
var elements = _driver.FindElements(
MobileBy.AccessibilityId("TaskItem_0"));
elements.Should().BeEmpty();
}
public void Dispose()
{
_driver?.Quit();
}
}
כדי שהאלמנטים יהיו נגישים לבדיקות, חשוב להגדיר AutomationId ב-XAML. זה גם עוזר ל-Accessibility, אז זה win-win:
<Button Text="הוסף משימה"
AutomationId="AddTaskButton"
Command="{Binding AddTaskCommand}" />
<Entry Placeholder="כותרת המשימה"
AutomationId="TaskTitleEntry"
Text="{Binding Title}" />
שיטות עבודה מומלצות
אחרי כמה פרויקטים עם בדיקות ב-MAUI, הנה הדברים שלמדתי (חלקם בדרך הקשה):
- בדקו לוגיקה, לא UI: 80% מהבדיקות שלכם צריכות להיות בדיקות יחידה ל-ViewModels ו-Services. הן מהירות, יציבות וקלות לתחזוקה. רק 20% צריכות להיות בדיקות UI — ואלה רק לתרחישים הכי קריטיים.
- שם הבדיקה חשוב: השתמשו בפורמט
MethodName_Should_When. למשל:LoadTasks_ShouldSetIsEmpty_WhenNoTasksExist. זה הופך את רשימת הבדיקות לתיעוד חי של ההתנהגות הצפויה. - בדיקה אחת — אסרציה אחת: כל בדיקה צריכה לבדוק דבר אחד ספציפי. אם בדיקה נכשלת, צריך להיות ברור מיד מה בדיוק השתבש.
- אל תבדקו את ה-Framework: אין צורך לבדוק ש-
ObservableCollectionעובד או ש-SQLiteשומר נתונים. בדקו את הלוגיקה שלכם בלבד. - הריצו בדיקות ב-CI/CD: הגדירו Pipeline שמריץ את כל הבדיקות בכל Push. אם משהו נשבר, תדעו מיד:
# .github/workflows/test.yml
name: Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- run: dotnet test TaskMaster.Tests/
--configuration Release
--logger "trx;LogFileName=results.trx"
--collect:"XPlat Code Coverage"
- uses: actions/upload-artifact@v4
with:
name: test-results
path: "**/*.trx"
- כיסוי קוד (Code Coverage): שאפו ל-80% כיסוי על הלוגיקה העסקית. לא 100% (זה כמעט תמיד בזבוז זמן ומוביל לבדיקות שבריריות) ולא 50% (זה פשוט לא מספיק). הפקודה
--collect:"XPlat Code Coverage"מייצרת דוחות כיסוי שאפשר לנתח. - השתמשו ב-Test Data Builders: במקום לשכפל יצירת אובייקטי בדיקה בכל טסט, צרו Builder. זה חוסך שכפול קוד ומקל על שינויים עתידיים:
public class TaskItemBuilder
{
private readonly TaskItem _task = new()
{
Title = "משימת ברירת מחדל",
Description = "תיאור ברירת מחדל",
Priority = 1,
DueDate = DateTime.Now.AddDays(7),
CreatedAt = DateTime.Now
};
public TaskItemBuilder WithTitle(string title)
{
_task.Title = title;
return this;
}
public TaskItemBuilder WithPriority(int priority)
{
_task.Priority = priority;
return this;
}
public TaskItemBuilder Completed()
{
_task.IsCompleted = true;
return this;
}
public TaskItemBuilder DueOn(DateTime date)
{
_task.DueDate = date;
return this;
}
public TaskItem Build() => _task;
}
// שימוש בבדיקה:
var urgentTask = new TaskItemBuilder()
.WithTitle("דחוף!")
.WithPriority(2)
.DueOn(DateTime.Now.AddHours(2))
.Build();
כלי הדיאגנוסטיקה החדשים של .NET MAUI 10
.NET MAUI 10 הוסיף מערכת דיאגנוסטיקה מובנית שאפשר לנצל גם בבדיקות. המערכת מבוססת על ActivitySource ו-Meters של .NET, ומספקת מטריקות לביצועי Layout. זה נחמד במיוחד כשצריך לאתר בעיות ביצועים:
// בדיקת ביצועים עם Metrics של MAUI 10
[Fact]
public void MeasurePerformance_ShouldComplete_WithinThreshold()
{
// Arrange
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var tasks = Enumerable.Range(1, 1000)
.Select(i => new TaskItem
{
Id = i,
Title = $"משימה {i}",
Priority = i % 3
})
.ToList();
// Act
var sorted = tasks
.OrderByDescending(t => t.Priority)
.ThenBy(t => t.DueDate)
.ToList();
stopwatch.Stop();
// Assert - מיון 1000 משימות צריך לקחת פחות מ-50ms
stopwatch.ElapsedMilliseconds.Should().BeLessThan(50);
sorted.Should().HaveCount(1000);
}
המטריקות החדשות כוללות maui.layout.measure_count, maui.layout.measure_duration, maui.layout.arrange_count ו-maui.layout.arrange_duration. שימושי מאוד לזיהוי צווארי בקבוק בביצועי Layout.
שאלות נפוצות
האם אפשר להריץ בדיקות יחידה ל-ViewModels בלי אמולטור?
בהחלט כן! זה בדיוק אחד היתרונות הגדולים של ארכיטקטורת MVVM. כל הלוגיקה העסקית נמצאת ב-ViewModels שהם מחלקות C# רגילות. פרויקט הבדיקות הוא פרויקט xUnit סטנדרטי שרץ על ה-CLR — אין צורך באמולטור או מכשיר פיזי. רק בדיקות UI עם Appium דורשות אמולטור.
מה ההבדל בין Moq ל-NSubstitute, ומה עדיף ל-MAUI?
שתיהן ספריות Mocking מצוינות. Moq היא הותיקה והפופולרית יותר, עם תחביר מבוסס Fluent API (Setup...Returns). NSubstitute יותר מינימליסטית עם תחביר פשוט יותר. לפרויקטי MAUI, שתיהן עובדות מעולה. באופן אישי, אני מעדיף Moq בגלל השליטה המדויקת יותר על Verification — אבל זו העדפה אישית ו-NSubstitute קלה יותר ללמידה למתחילים.
כמה אחוזי כיסוי קוד (Code Coverage) צריך לשאוף אליהם?
כלל האצבע שלי הוא 80% כיסוי על הלוגיקה העסקית (ViewModels ו-Services). אל תשקיעו בכיסוי של שכבת ה-UI (XAML) או קוד דבק פשוט. 100% כיסוי זו מטרה שנשמעת נחמדה אבל בפועל מובילה לבדיקות שבריריות ומעצבנות. התמקדו בנתיבי קוד קריטיים — תנאים, טיפול בשגיאות, ולוגיקה עסקית מורכבת.
איך בודקים ניווט Shell ב-.NET MAUI בלי להריץ את האפליקציה?
ניווט Shell קשה לבדיקה ישירה כי הוא תלוי בתשתית MAUI. הגישה המומלצת היא ליצור ממשק INavigationService שעוטף את Shell.Current.GoToAsync. בבדיקות, אתם פשוט יוצרים Mock של הממשק הזה ומוודאים שה-ViewModel קורא לפונקציית הניווט עם הפרמטרים הנכונים. נקי ופשוט.
האם Appium הוא הכלי היחיד לבדיקות UI ב-MAUI?
לא, אבל הוא הנפוץ ביותר. יש גם Xamarin.UITest (שעובד עם MAUI בתצורות מסוימות) ו-DeviceRunners — כלי בדיקות שרץ ישירות על המכשיר כאפליקציית MAUI. מיקרוסופט גם עובדת על שיפורים בתשתית הבדיקות המובנית. עם זאת, Appium נשאר הסטנדרט התעשייתי בגלל התמיכה הרחבה שלו במגוון פלטפורמות ושפות — וזה מה שרוב החברות משתמשות בו.