Skip to main content

avalonia_mcp_tools/
testing_integration_tool.rs

1//! Testing Integration tool - Test setup patterns
2use avalonia_mcp_core::error::AvaloniaMcpError;
3use avalonia_mcp_core::markdown::MarkdownOutputBuilder;
4use rmcp::model::{CallToolResult, Content};
5use rmcp::tool;
6use serde::{Deserialize, Serialize};
7use schemars::JsonSchema;
8
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct TestingIntegrationParams {
11    pub test_type: Option<String>,
12    pub include_examples: Option<bool>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
16pub struct UITestParams { pub test_type: Option<String>, pub include_page_objects: Option<bool> }
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19pub struct MocksAndBuildersParams { pub entity_name: String, pub include_fluent_builder: Option<bool> }
20
21#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
22pub struct PerformanceTestParams { pub test_area: Option<String>, pub include_profiling: Option<bool> }
23
24#[derive(Debug, Clone, Default)]
25pub struct TestingIntegrationTool;
26
27impl TestingIntegrationTool {
28    pub fn new() -> Self { Self }
29
30    #[tool(description = "Generate test setup patterns for AvaloniaUI applications. Covers unit tests, UI tests with Avalonia.Headless, and mocking strategies.")]
31    pub async fn generate_testing_integration(
32        &self,
33        params: TestingIntegrationParams,
34    ) -> Result<CallToolResult, AvaloniaMcpError> {
35        let include_examples = params.include_examples.unwrap_or(true);
36        let test_type = params.test_type.as_deref().unwrap_or("all");
37
38        let output = match test_type {
39            "unit" => self.generate_unit_tests(include_examples),
40            "ui" => self.generate_ui_tests(include_examples),
41            "integration" => self.generate_integration_tests(include_examples),
42            _ => self.generate_all_tests(include_examples),
43        };
44
45        Ok(CallToolResult::success(vec![Content::text(output)]))
46    }
47
48    fn generate_unit_tests(&self, include_examples: bool) -> String {
49        let mut builder = MarkdownOutputBuilder::new()
50            .heading(1, "Unit Testing Patterns")
51            .paragraph("Unit testing best practices for AvaloniaUI applications.")
52            .heading(2, "Test Framework");
53
54        if include_examples {
55            builder = builder
56                .heading(3, "xUnit Test Setup")
57                .code_block("csharp", r#"// Test project setup
58// dotnet add package xunit
59// dotnet add package xunit.runner.visualstudio
60// dotnet add package Moq
61
62public class ViewModelTests
63{
64    [Fact]
65    public void ViewModel_PropertyChange_UpdatesView()
66    {
67        // Arrange
68        var mockService = new Mock<IDataService>();
69        mockService.Setup(s => s.GetData()).ReturnsAsync(new Data());
70        var vm = new MainViewModel(mockService.Object);
71        
72        // Act
73        vm.LoadCommand.Execute(null);
74        
75        // Assert
76        Assert.NotNull(vm.Data);
77        mockService.Verify(s => s.GetData(), Times.Once);
78    }
79    
80    [Fact]
81    public async Task ViewModel_ErrorHandling_ShowsMessage()
82    {
83        // Arrange
84        var mockService = new Mock<IDataService>();
85        mockService.Setup(s => s.GetData()).ThrowsAsync(new Exception("Test"));
86        var vm = new MainViewModel(mockService.Object);
87        
88        // Act
89        await vm.LoadCommand.ExecuteAsync(null);
90        
91        // Assert
92        Assert.NotNull(vm.ErrorMessage);
93    }
94}"#)
95                .heading(3, "Mocking Services")
96                .code_block("csharp", r#"// Using Moq for mocking
97var mockRepo = new Mock<IRepository>();
98mockRepo.Setup(r => r.GetById(It.IsAny<int>())).ReturnsAsync(new Entity());
99mockRepo.Setup(r => r.Save(It.IsAny<Entity>())).Returns(Task.CompletedTask);
100
101// Using NSubstitute (alternative)
102var mockRepo = Substitute.For<IRepository>();
103mockRepo.GetById(1).Returns(new Entity());
104await mockRepo.Save(Arg.Any<Entity>()).Returns(Task.CompletedTask);"#);
105        }
106
107        builder.heading(2, "Best Practices")
108            .task_list(vec![(true, "Test one thing per test"), (true, "Use descriptive names"), (true, "Arrange-Act-Assert pattern"), (true, "Mock external dependencies"), (false, "Aim for 80%+ coverage")])
109            .build()
110    }
111
112    fn generate_ui_tests(&self, include_examples: bool) -> String {
113        let mut builder = MarkdownOutputBuilder::new()
114            .heading(1, "UI Testing Patterns")
115            .paragraph("UI testing with Avalonia.Headless for automated UI tests.")
116            .heading(2, "Headless Testing");
117
118        if include_examples {
119            builder = builder
120                .heading(3, "Avalonia.Headless Setup")
121                .code_block("csharp", r#"// Test project: dotnet add package Avalonia.Headless.XUnit
122
123public class MainWindowTests : IClassFixture<AppFixture>
124{
125    private readonly AppFixture _fixture;
126    
127    public MainWindowTests(AppFixture fixture) => _fixture = fixture;
128    
129    [Fact]
130    public async Task Button_Click_UpdatesCounter()
131    {
132        // Arrange
133        var window = new MainWindow();
134        window.Show(_fixture.App);
135        
136        // Act
137        var button = window.FindControl<Button>("IncrementButton");
138        button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
139        await _fixture.App.Dispatcher.UIThread.RunJobs();
140        
141        // Assert
142        var textBlock = window.FindControl<TextBlock>("CounterText");
143        Assert.Equal("1", textBlock.Text);
144    }
145    
146    [Fact]
147    public async Task TextBox_Input_Validates()
148    {
149        // Arrange
150        var window = new MainWindow();
151        window.Show(_fixture.App);
152        
153        // Act
154        var textBox = window.FindControl<TextBox>("NameInput");
155        textBox.Text = "Test";
156        textBox.RaiseEvent(new RoutedEventArgs(TextInputEvent));
157        
158        // Assert
159        Assert.False(textBox.Classes.Contains("error"));
160    }
161}"#);
162        }
163
164        builder.heading(2, "UI Test Best Practices")
165            .task_list(vec![(true, "Use Page Object pattern"), (true, "Wait for async operations"), (true, "Test user workflows"), (true, "Verify visual states"), (false, "Run in CI pipeline")])
166            .build()
167    }
168
169    fn generate_integration_tests(&self, include_examples: bool) -> String {
170        let mut builder = MarkdownOutputBuilder::new()
171            .heading(1, "Integration Testing Patterns")
172            .paragraph("Integration testing for full application workflows.")
173            .heading(2, "Test Setup");
174
175        if include_examples {
176            builder = builder
177                .heading(3, "Integration Test Base")
178                .code_block("csharp", r#"public class IntegrationTestBase : IDisposable
179{
180    protected readonly IHost _host;
181    protected readonly IServiceProvider _services;
182    
183    public IntegrationTestBase()
184    {
185        _host = Host.CreateDefaultBuilder()
186            .ConfigureServices(ConfigureServices)
187            .Build();
188        _host.Start();
189        _services = _host.Services;
190    }
191    
192    protected virtual void ConfigureServices(IServiceCollection services)
193    {
194        // Register test services
195        services.AddSingleton<ITestDatabase, TestDatabase>();
196    }
197    
198    public void Dispose() => _host.Dispose();
199}"#);
200        }
201
202        builder.heading(2, "Integration Test Checklist")
203            .task_list(vec![(true, "Use test database"), (true, "Mock external services"), (true, "Test full workflows"), (true, "Clean up after tests"), (false, "Run in isolated environment")])
204            .build()
205    }
206
207    fn generate_all_tests(&self, include_examples: bool) -> String {
208        MarkdownOutputBuilder::new()
209            .heading(1, "Complete Testing Guide")
210            .paragraph("Comprehensive testing strategy for AvaloniaUI applications.")
211            .heading(2, "Testing Pyramid")
212            .list(vec!["Unit Tests (70%)", "Integration Tests (20%)", "UI Tests (10%)"])
213            .heading(2, "Unit Tests")
214            .paragraph(&self.generate_unit_tests(include_examples))
215            .heading(2, "UI Tests")
216            .paragraph(&self.generate_ui_tests(include_examples))
217            .heading(2, "Integration Tests")
218            .paragraph(&self.generate_integration_tests(include_examples))
219            .heading(2, "CI/CD Integration")
220            .code_block("yaml", r#"# GitHub Actions
221name: Tests
222on: [push, pull_request]
223jobs:
224  test:
225    runs-on: ubuntu-latest
226    steps:
227    - uses: actions/checkout@v4
228    - name: Run tests
229      run: dotnet test --collect:"XPlat Code Coverage"
230    - name: Upload coverage
231      uses: codecov/codecov-action@v4"#)
232            .build()
233    }
234
235    #[tool(description = "Creates UI automation tests for AvaloniaUI applications using Avalonia.Headless")]
236    pub async fn generate_ui_automation_tests(&self, params: UITestParams) -> Result<CallToolResult, AvaloniaMcpError> {
237        let test_type = params.test_type.as_deref().unwrap_or("button_click").to_lowercase();
238        let include_po = params.include_page_objects.unwrap_or(true);
239        let po = if include_po { "\n## Page Objects\n```csharp\npublic class LoginPageObject\n{\n    private readonly IControl _root;\n    public LoginPageObject(IControl root) => _root = root;\n    public IControl UsernameInput => _root.FindControl<TextBox>(\"UsernameBox\");\n    public IControl LoginButton => _root.FindControl<Button>(\"LoginBtn\");\n    public void Login(string user, string pass)\n    {\n        UsernameInput.Text = user;\n        LoginButton.Click();\n    }\n}\n```" } else { "" };
240        let builder = MarkdownOutputBuilder::new()
241            .heading(1, "UI Automation Tests")
242            .heading(2, "Configuration").task_list(vec![(true, format!("Type: {}", test_type)), (true, format!("Page Objects: {}", include_po))])
243            .heading(2, "Headless Test").code_block("csharp", "[TestClass]\npublic class UITests\n{\n    [TestMethod]\n    public async Task Button_Click_ShouldUpdateText()\n    {\n        var window = new MainWindow();\n        var button = window.Find<Button>(\"MyButton\");\n        button.Click();\n        Assert.AreEqual(\"Clicked\", window.Find<TextBlock>(\"Status\").Text);\n    }\n}");
244        let builder = if include_po { builder.heading(2, "Page Objects").code_block("csharp", &po.replace("\n## Page Objects\n```csharp\n", "").replace("\n```", "")) } else { builder };
245        let builder = builder.heading(2, "Setup").list(&["Install Avalonia.Headless.NUnit", "Use BuildMainWindow() for test windows", "Simulate input with Click(), Focus(), Text"]);
246        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
247    }
248
249    #[tool(description = "Generates mock objects and test data builders for AvaloniaUI testing")]
250    pub async fn generate_mocks_and_builders(&self, params: MocksAndBuildersParams) -> Result<CallToolResult, AvaloniaMcpError> {
251        if params.entity_name.is_empty() { return Err(AvaloniaMcpError::validation("Entity name cannot be empty")); }
252        let include_fluent = params.include_fluent_builder.unwrap_or(true);
253        let e = &params.entity_name;
254        let builder_code = if include_fluent {
255            format!("public class {e}Builder\n{{\n    private int _id = 1;\n    private string _name = \"Test {e}\";\n    public {e}Builder WithId(int id) {{ _id = id; return this; }}\n    public {e}Builder WithName(string name) {{ _name = name; return this; }}\n    public {e} Build() => new() {{ Id = _id, Name = _name }};\n    public static implicit operator {e}({e}Builder b) => b.Build();\n}}")
256        } else {
257            format!("public static class {e}Factory\n{{\n    public static {e} Create(int id = 1, string name = \"Test\") =>\n        new() {{ Id = id, Name = name }};\n}}")
258        };
259        let builder = MarkdownOutputBuilder::new()
260            .heading(1, &format!("Test Builders: {}", e))
261            .heading(2, "Configuration").task_list(vec![(true, format!("Entity: {}", e)), (true, format!("Fluent: {}", include_fluent))])
262            .heading(2, "Implementation").code_block("csharp", &builder_code)
263            .heading(2, "Usage").code_block("csharp", &format!("var entity = new {e}Builder().WithId(42).WithName(\"Custom\").Build();"))
264            .heading(2, "Mock Setup").code_block("csharp", &format!("var mock = new Mock<I{e}Service>();\nmock.Setup(s => s.GetAsync(It.IsAny<int>())).ReturnsAsync(new {e}Builder().Build());"));
265        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
266    }
267
268    #[tool(description = "Creates performance and load tests for AvaloniaUI applications")]
269    pub async fn generate_performance_tests(&self, params: PerformanceTestParams) -> Result<CallToolResult, AvaloniaMcpError> {
270        let test_area = params.test_area.as_deref().unwrap_or("rendering").to_lowercase();
271        let include_profiling = params.include_profiling.unwrap_or(true);
272        let profiling = if include_profiling { "\n## Profiling\n```csharp\npublic static class PerformanceTester\n{\n    public static async Task<long> MeasureAsync(Func<Task> op, int iterations = 100)\n    {\n        var sw = Stopwatch.StartNew();\n        for (int i = 0; i < iterations; i++) await op();\n        sw.Stop();\n        return sw.ElapsedMilliseconds;\n    }\n}\n```" } else { "" };
273        let builder = MarkdownOutputBuilder::new()
274            .heading(1, "Performance Tests")
275            .heading(2, "Configuration").task_list(vec![(true, format!("Area: {}", test_area)), (true, format!("Profiling: {}", include_profiling))])
276            .heading(2, "Performance Test").code_block("csharp", "[TestClass]\npublic class PerformanceTests\n{\n    [TestMethod]\n    public async Task Render_With1000Items_ShouldCompleteInTime()\n    {\n        var sw = Stopwatch.StartNew();\n        var listBox = new ListBox { Items = Enumerable.Range(0, 1000) };\n        listBox.Measure(new Size(800, 600));\n        listBox.Arrange(new Rect(0, 0, 800, 600));\n        sw.Stop();\n        Assert.IsTrue(sw.ElapsedMilliseconds < 500);\n    }\n}");
277        let builder = if include_profiling { builder.heading(2, "Profiling").code_block("csharp", &profiling.replace("\n## Profiling\n```csharp\n", "").replace("\n```", "")) } else { builder };
278        let builder = builder.heading(2, "Metrics").list(&["<500ms for 1000 item rendering", "Memory stable over time", "CPU <10% idle", "60fps animations"]);
279        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    #[tokio::test]
287    async fn test_generate_testing() {
288        let tool = TestingIntegrationTool::new();
289        let result = tool.generate_testing_integration(TestingIntegrationParams { test_type: None, include_examples: Some(true) }).await.unwrap();
290        assert!(result.is_error.is_none() || result.is_error == Some(false));
291    }
292
293    #[tokio::test]
294    async fn test_generate_ui_automation_tests_success() {
295        let tool = TestingIntegrationTool::new();
296        let params = UITestParams {
297            test_type: Some("button_click".to_string()),
298            include_page_objects: Some(true),
299        };
300        let result = tool.generate_ui_automation_tests(params).await.unwrap();
301        assert!(result.is_error.is_none() || result.is_error == Some(false));
302    }
303
304    #[tokio::test]
305    async fn test_generate_mocks_and_builders_success() {
306        let tool = TestingIntegrationTool::new();
307        let params = MocksAndBuildersParams {
308            entity_name: "Order".to_string(),
309            include_fluent_builder: Some(true),
310        };
311        let result = tool.generate_mocks_and_builders(params).await.unwrap();
312        assert!(result.is_error.is_none() || result.is_error == Some(false));
313    }
314
315    #[tokio::test]
316    async fn test_generate_mocks_and_builders_empty_name() {
317        let tool = TestingIntegrationTool::new();
318        let params = MocksAndBuildersParams {
319            entity_name: "".to_string(),
320            include_fluent_builder: None,
321        };
322        let result = tool.generate_mocks_and_builders(params).await;
323        assert!(result.is_err());
324    }
325
326    #[tokio::test]
327    async fn test_generate_performance_tests_success() {
328        let tool = TestingIntegrationTool::new();
329        let params = PerformanceTestParams {
330            test_area: Some("rendering".to_string()),
331            include_profiling: Some(true),
332        };
333        let result = tool.generate_performance_tests(params).await.unwrap();
334        assert!(result.is_error.is_none() || result.is_error == Some(false));
335    }
336}