{"id":423846,"date":"2024-02-21T14:54:37","date_gmt":"2024-02-21T13:54:37","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=dotnet&#038;p=423846"},"modified":"2024-03-19T18:00:56","modified_gmt":"2024-03-19T17:00:56","slug":"jetbrains-ai-assistant-generate-test-data-for-dotnet","status":"publish","type":"dotnet","link":"https:\/\/blog.jetbrains.com\/en\/dotnet\/2024\/02\/21\/jetbrains-ai-assistant-generate-test-data-for-dotnet","title":{"rendered":"How To Use AI Assistant to Generate Test Data For .NET Applications"},"content":{"rendered":"\n<p>In software development, writing tests is equivalent to exercising and eating vegetables; everyone knows they should be doing more but likely don\u2019t do enough. One of the reasons folks avoid writing tests is the tedium of constructing test cases that help make a test valuable and worth maintaining. A test that is too simple can steer towards uselessness, while a valuable test can be complex to construct. As developers, we need to balance value and time spent producing the value. Luckily, now is the perfect time to reevaluate how we write tests and, more specifically, how we generate test data for our test suites.&nbsp;<\/p>\n\n\n\n<p>In this post, we\u2019ll walk you through scenarios to generate test data with the <a href=\"https:\/\/jetbrains.com\/ai\" target=\"_blank\" rel=\"noopener\">JetBrains AI Assistant<\/a> for .NET unit tests and how you can leverage generated data to improve the isolation of your tests and ultimately produce a more valuable and less brittle test suite.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Working with Unit Tests<\/h2>\n\n\n\n<p>We\u2019ll use the xUnit test framework and JetBrains Annotations in the following examples. These are the libraries I typically use, but all examples apply to other libraries.<\/p>\n\n\n\n<p>Regardless of your style of testing, it\u2019s essential to maintain a consistent approach across tests. In the cross-section of tests and generative assistants, there are three factors to consider when constructing a test suite:<\/p>\n\n\n\n<ol>\n<li>Always think about the \u201cwhy\u201d. Tests are not inherently valuable, and carelessly adding tests to hit a metric can lead to bloat and a long-term maintenance burden.&nbsp;<\/li>\n\n\n\n<li>When writing tests, you should see a test fail before it succeeds. It may seem obvious that a test may pass, but it&#8217;s a good habit to form to avoid bugs. Seeing a failure is especially important when AI Assistant can generate assertion statements.<\/li>\n\n\n\n<li>Test isolation increases the reliability of tests by reducing interdependency between each test.<\/li>\n<\/ol>\n\n\n\n<p>JetBrains AI Assistant can accelerate your test creation, but please be mindful of how and when you use it. I\u2019ll show you techniques to generate and maintain tests against a service <em>carefully<\/em>.&nbsp;<\/p>\n\n\n\n<p>These techniques will help keep you engaged and focused rather than autocompleting your way to a suite of tests you don\u2019t understand.<\/p>\n\n\n\n<p>Let\u2019s first look at the class we\u2019ll be testing, a <code>PersonService<\/code> with several methods of varying complexity.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System.Text;\n\nnamespace GeneratedDataTests;\n\npublic record Person(string Name, int Age);\n\npublic class PersonService\n{\n    public double AverageAge(List&lt;Person> people)\n    {\n        return people.Average(p => p.Age);\n    }\n\n    public string? LongestName(List&lt;Person> people)\n    {\n        return people\n            .OrderByDescending(p => p.Name)\n            .ThenByDescending(p => p.Name.Length)\n            .Select(p => p.Name)\n            .FirstOrDefault();\n    }\n\n    public string CreateCsv(List&lt;Person> person)\n    {\n        var sb = new StringBuilder($\"{nameof(Person.Age)},{nameof(Person.Name)}\");\n        foreach (var (name, age) in person)\n        {\n            sb.AppendLine($@\"\"\"{name}\"\",\"\"{age}\"\"\");\n        }\n        return sb.ToString();\n    }\n\n    public List&lt;Person> ProcessCsv(string csv)\n    {\n        var lines = csv.Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);\n        var people = new List&lt;Person>();\n        \/\/ skip header\n        foreach (var line in lines.Skip(1))\n        {\n            var values = line.Split(',');\n            if (values.Length == 2)\n            {\n                var name = values[0].Trim('\"');\n                var age = int.Parse(values[1].Trim('\"'));\n                people.Add(new Person(name, age));\n            }\n        }\n        return people;\n    }\n}\n<\/pre>\n\n\n\n<p>You may find similar services in your applications, with logic encapsulated within helper methods. Let\u2019s start by first testing the <code>AverageAge<\/code> method. In a test class, start by adding this boilerplate code.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using JetBrains.Annotations;\n\nnamespace GeneratedDataTests;\n\n[TestSubject(typeof(PersonService))]\npublic class PersonServiceTests\n{\n    private readonly PersonService sut = new();\n    \n    [Fact]\n    public void Find_Average_Age()\n    {\n        \/\/ Arrange: a 6 entry list of Person with random odd and even\n        \/\/ ages between 30 to 90 and Japanese names.\n        \n        \/\/ Act\n        var actual = sut.AverageAge(people);\n        \n        \/\/ Assert\n        Assert.Equal(0, actual);\n    }\n}\n<\/pre>\n\n\n\n<p>A few critical elements to mention about this boilerplate:<\/p>\n\n\n\n<ol>\n<li>The <strong>Arrange<\/strong> portion of our test is an AI Assistant prompt that will generate our dataset.<\/li>\n\n\n\n<li>The <strong>Act <\/strong>portion executes the method of <code>AverageAge<\/code> as expected.<\/li>\n\n\n\n<li>The <strong>Assert<\/strong> portion already contains an assertion that is intentionally incorrect.<\/li>\n<\/ol>\n\n\n\n<p>While AI Assistant could undoubtedly generate an entire test, we want to remain thoughtful about what we\u2019re creating.<\/p>\n\n\n\n<p>After the prompting comment, we can press <strong>Enter<\/strong> to see what JetBrains AI Assistant offers as a potential dataset.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1518\" height=\"1230\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-8.png\" alt=\"Test class with recommended test data from JetBrains AI Assistant\" class=\"wp-image-423863\"\/><\/figure>\n\n\n\n<p>Our next step is to run the functioning test to see if it fails.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"512\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-9.png\" alt=\"Unit test tool window with a assertion failure.\" class=\"wp-image-423874\"\/><\/figure>\n\n\n\n<p>Here, we can see both a failure of the test and a simplification in our assertion. Let\u2019s update our test to account for the precision of our <code>double<\/code> value.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">[Fact]\npublic void Find_Average_Age()\n{\n    \/\/ Arrange: a 6 entry list of Person with random odd and even\n    \/\/ ages between 30 to 90 and Japanese names.\n    var people = new List&lt;Person>\n    {\n        new Person(\"Yuki\", 35),\n        new Person(\"Hiroshi\", 42),\n        new Person(\"Akiko\", 51),\n        new Person(\"Satoshi\", 66),\n        new Person(\"Naomi\", 73),\n        new Person(\"Kenji\", 88)\n    };\n        \n    \/\/ Act\n    var actual = sut.AverageAge(people);\n        \n    \/\/ Assert\n    Assert.Equal(59.17, Math.Round(actual, 2));\n}\n<\/pre>\n\n\n\n<p>Rerunning the test shows we now have a passing test.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1250\" height=\"588\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-10.png\" alt=\"Unit test tool window with a passing test\" class=\"wp-image-423885\"\/><\/figure>\n\n\n\n<p>Now, let\u2019s write another test. Under the <code>Find_Average_Age<\/code> method, hit the <strong>Enter <\/strong>key again. You\u2019ll notice something surprising.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1396\" height=\"1032\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-11.png\" alt=\"JetBrains AI Assistant recommending the next test in its entirety\" class=\"wp-image-423896\"\/><\/figure>\n\n\n\n<p>JetBrains AI Assistant\u2019s code completion has already determined the next test. In this case, it\u2019s very similar to our original test. Accepting this code may be tempting if you&#8217;re in a rush, but it could also be a mistake. In this case, running the generated test leads to failure.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"1190\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-15.png\" alt=\"The recommended test with a failed run\" class=\"wp-image-423940\"\/><\/figure>\n\n\n\n<p>Remember, you\u2019re still an essential part of the test-writing process. AI Assistant only proposes a \u201cpossible\u201d solution, but you must consider the proposal critically. This test fails because AI Assistant could not account for the implementation of <code>LongestName<\/code>. After all, we did not provide the implementation within the context. Let\u2019s look at it again.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public string? LongestName(List&lt;Person> people)\n{\n    return people\n        .OrderByDescending(p => p.Name)\n        .ThenByDescending(p => p.Name.Length)\n         .Select(p => p.Name)\n        .FirstOrDefault();\n}<\/pre>\n\n\n\n<p>As you notice, we sort names alphabetically from Z to A and then by length. Generating test data is good, but you should be wary of automatically generated assertions. That is why I recommend writing your assertions before generating test data. Slowing down gives you time to think about the goal and value of the test. Let\u2019s fix the assertion and move on to a more interesting test case scenario, parsing comma-separated value strings.<\/p>\n\n\n\n<p>Let\u2019s start with the boilerplate for a test similar to the previous ones.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">[Fact]\npublic void Parse_Csv()\n{\n    \/\/ Arrange: a string CSV with a header row \"Name\",\"Age\". Quote all values like \"12\". 5 rows only.\n    \n    \/\/ Act\n    var actual = sut.ProcessCsv(csv);\n\n    \/\/ Assert\n    Assert.Equal(0, actual.Count);\n}<\/pre>\n\n\n\n<p>Pressing <strong>Enter <\/strong>after the comment produces a string literal for our method.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"653\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-14.png\" alt=\"JetBrains AI Assistant recommending a CSV string in Csharp\" class=\"wp-image-423929\"\/><\/figure>\n\n\n\n<p>You may use the quick fixes provided by ReSharper to change the string to use newer C# 12 features, but for the purposes of this test, it\u2019s unnecessary.<\/p>\n\n\n\n<p>Running the test fails because our assertion checks for a value of 0. Let\u2019s change the assertion after a failed test run to the following.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Assert\nAssert.Equal(5, actual.Count);<\/pre>\n\n\n\n<p>Now we should have a passing test. Hooray!<\/p>\n\n\n\n<p>If you must assert the items in the collection parsed correctly, you can use AI Assistant to generate the assertions based on the test data.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1524\" height=\"170\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-13.png\" alt=\"More assertion recommendations for elements in the collection\" class=\"wp-image-423918\"\/><\/figure>\n\n\n\n<p>The added assertions can undoubtedly add to more accurate tests, but they are lengthy, and you should use them sparingly. A new test asserting the accuracy of parsing would be more beneficial. Use your judgment here regarding how to approach this in your test suite.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">More Test Data Scenarios<\/h2>\n\n\n\n<p>When working within complex data-driven systems, you\u2019ll likely represent data in multiple formats. Some standard formats that come to mind include JSON, XML, and SQL schema. The need to work with numerous formats is where the JetBrains AI Assistant can shine, cutting out the doldrums of creating meaningful data and converting between formats.<\/p>\n\n\n\n<p>In a new AI Assistant chat, let\u2019s use the <code>Person<\/code> type to generate a JSON object with a collection of items.<\/p>\n\n\n\n<p>We\u2019ll use the following prompt:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Given the following record type, generate a JSON object with a `results` field that contains a 2-entry collection of Person objects.&nbsp;<\/p>\n\n\n\n<p><code>```csharp public record Person(string Name, int Age);```<\/code><\/p>\n<\/blockquote>\n\n\n\n<p>The JetBrains AI Assistant generates a JSON object ready to use for our tests that you can now place in a raw-string literal or within a file.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"942\" height=\"716\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-16.png\" alt=\"JSON sample generated in AI Chat\" class=\"wp-image-423951\"\/><\/figure>\n\n\n\n<p>We can continue the chat session with the following prompt:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Give me the same JSON object but now as XML with all values as attributes on elements of Person<\/p>\n<\/blockquote>\n\n\n\n<p>Which results in the following XML file.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"746\" height=\"352\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-17.png\" alt=\"XML data recommended in chat\" class=\"wp-image-423962\"\/><\/figure>\n\n\n\n<p>Finally, let\u2019s create a SQL table to store valuable new information. Within the same chat session, we can add yet another prompt. Remember, not all SQL is created equal. When working with SQL, add a hint denoting the desired syntax.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Could you write a SQL Schema creation script for a Person table with an additional <code>Id<\/code> column that is an identity column? Make sure it is SQLite syntax.<\/p>\n<\/blockquote>\n\n\n\n<p>The resulting output is as you\u2019d expect.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"783\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-18.png\" alt=\"SQLite schema recommended in chat\" class=\"wp-image-423973\"\/><\/figure>\n\n\n\n<p>Let\u2019s go further and generate new test data for our new SQLite table, maybe from a galaxy far far away.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Could you generate 5 insert statements for the Person table where the names are Star Wars characters?<\/p>\n<\/blockquote>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"753\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/01\/image-19.png\" alt=\"SQL inserts recommended by AI Chat\" class=\"wp-image-423984\"\/><\/figure>\n\n\n\n<p>Wow! That\u2019s great because less time spent typing out test data means more time playing with my Star Wars figures. I mean doing serious work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>As you\u2019ve seen in this post, AI Assistant can speed up your existing workflows by removing some of the tedium around generating test data. You <em>could<\/em><strong><em> <\/em><\/strong>undoubtedly type all this yourself, but these are typically the boring parts of software development. The crucial parts are problem-solving and delivering reliable solutions to your users, and I think this post demonstrates that you can do just that with JetBrains AI Assistant.&nbsp;<\/p>\n\n\n\n<p>Combined with the two decades of expertise baked into JetBrains products like ReSharper and JetBrains Rider, we believe AI Assistants can be another step towards building better applications. <a href=\"https:\/\/www.jetbrains.com\/ai\/\" target=\"_blank\" rel=\"noopener\">Read more on the official website to learn more about  JetBrains AI Assistant.<\/a><\/p>\n\n\n\n<p>We hope you enjoyed this post, and if you have any comments, questions, or suggestions, please feel free to leave a comment.<\/p>\n\n\n\n<p><sub>image credit: <a href=\"https:\/\/unsplash.com\/@katya\" target=\"_blank\" rel=\"noopener\">Katya Ross<\/a><\/sub><\/p>\n","protected":false},"author":1079,"featured_media":458394,"comment_status":"closed","ping_status":"closed","template":"","categories":[4992,1401],"tags":[8168,211,46,1978],"cross-post-tag":[8396],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/423846"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/types\/dotnet"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/users\/1079"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/comments?post=423846"}],"version-history":[{"count":9,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/423846\/revisions"}],"predecessor-version":[{"id":458407,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/423846\/revisions\/458407"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/media\/458394"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/media?parent=423846"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/categories?post=423846"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/tags?post=423846"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/cross-post-tag?post=423846"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}