blog

ChatGPT

decoration

In this post we want to show you how you can use ChatGPT to speed up writing Unit Tests. The prompts issued to the chat are to illustrate the scenario of cooperation between an experienced developer and a junior performing the task of writing tests. In the text below, we present the developer’s prompts and chat answers, with additional developer comments (bold). These comments are not included in prompts.

Developer: This is my C# class, please write me two example unit tests in xunit for constructor(*I check if the bot is able to correctly recognize the language and the code fragment I am talking about, I did not give it the name of the method but only the wording constructor (which is the initial method of the class), it recognized everything correctly*)

public class User
{
    public User(string sub, string email, int? roleId)
    {
        EnsureArg.IsNotNullOrWhiteSpace(sub, nameof(sub));
        EnsureArg.IsNotNullOrWhiteSpace(email, nameof(email));

        Sub = sub;
        Email = email;
        RoleId = roleId;
        StatusId = (int)UserStatusEnum.New;
        Photos = new List<UserPhoto>();
        Popularity = 0;
    }

    public void SetUserDetails(UserDetails userDetails)
    {
        EnsureArg.IsNotNullOrWhiteSpace(userDetails.Name, nameof(userDetails.Name));
        EnsureArg.IsNotNullOrWhiteSpace(userDetails.Surname, nameof(userDetails.Surname));
        EnsureCanBeUpdated();

        UserDetails = userDetails;
    }

    public void SetProfilePhoto(UserPhoto photo)
    {
        EnsureArg.IsNotNull(photo, nameof(photo));
        EnsureCanBeUpdated();

        var userPhoto = new UserPhoto(photo.FilePath, Id);
        Photos.Add(userPhoto);
    }

    public UserPhoto GetPhoto(long photoId)
    {
        var userPhoto = Photos.FirstOrDefault(p => p.Id == photoId);

        if (userPhoto is null)
        {
            throw new ArgumentException($"User with Id {Id} does not have photo with Id {photoId}");
        }

        return userPhoto;
    }

    public void EnsureCanBeUpdated()
    {
        if (StatusId == (int)UserStatusEnum.Restricted)
        {
            throw new DomainException("User can't be edited - invalid status");
        }

        if (IsRemoved)
        {
            throw new DomainException("User can't be edited - user is removed");
        }
    }

    public void SetUserAsRemoved()
    {
        IsRemoved = true;
    }

    public void IncreasePopularity()
    {
        Popularity++;
    }

    public long Id { get; set; }
    public string Sub { get; }
    public string Email { get; private set; }
    public int? RoleId { get; private set; }
    public long? UserDetailsId { get; set; }
    public int StatusId { get; private set; }
    public bool IsRemoved { get; private set; }
    public long Popularity { get; private set; }
    public UserDetails UserDetails { get; private set; }
    public List<UserPhoto> Photos { get; private set; }
}

Chat: Here are two sample unit tests for the constructor of the User class:

  1. A test to verify that the constructor creates a User object with default values ​​for the StatusId, Photos, and Popularity properties:
[Fact]
public void UserConstructor_DefaultValuesAreSet()
{
    // Act
    var user = new User("sub123", "test@example.com", 1);

    // Assert
    Assert.Equal(0, user.Popularity);
    Assert.Equal((int)UserStatusEnum.New, user.StatusId);
    Assert.NotNull(user.Photos);
    Assert.Empty(user.Photos);
}
  1. Test to see if the constructor throws an ArgumentException when sub is null:
[Fact]
public void UserConstructor_SubIsNull_ThrowsArgumentException()
{
    // Arrange
    string sub = null;
    string email = "test@example.com";
    int? roleId = 1;

    // Act & Assert
    Assert.Throws<ArgumentException>(() => new User(sub, email, roleId));
}

Developer: Modify these tests to use the Shouldly library (*Then I want him to modify the existing code with the library that I use when writing tests, it is worth noting that he will remember this and from now on tests will be created based on this library*)

Chat: Here are the modified unit tests for the constructor of the User class, using the Shouldly library:

  1. A test to verify that the constructor creates a User object with default values ​​for the StatusId, Photos, and Popularity properties:
[Fact]
public void UserConstructor_DefaultValuesAreSet()
{
    // Act
    var user = new User("sub123", "test@example.com", 1);

    // Assert
    user.Popularity.ShouldBe(0);
    user.StatusId.ShouldBe((int)UserStatusEnum.New);
    user.Photos.ShouldNotBeNull();
    user.Photos.ShouldBeEmpty();
}
  1. Test to see if the constructor throws an ArgumentException when sub is null:
[Fact]
public void UserConstructor_SubIsNull_ThrowsArgumentException()
{
    // Arrange
    string sub = null;
    string email = "test@example.com";
    int? roleId = 1;

    // Act & Assert
    Should.Throw<ArgumentException>(() => new User(sub, email, roleId));
}

Note: In both cases, you must use the ShouldNotBeNull() method to verify that the Photos property is not null.

Developer: Now write me the tests for the method EnsureCanBeUpdated() (*Now I ask him to create tests for a complex method with two IFs to see if the tests will cover both cases*)

Chat: Here are two example unit tests for the EnsureCanBeUpdated() method of the User class:

  1. Test to see if the method throws a DomainException when the StatusId property is Restricted
[Fact]
public void EnsureCanBeUpdated_StatusIsRestricted_ThrowsDomainException()
{
    // Arrange
    var user = new User("sub123", "test@example.com", 1);
    user.StatusId = (int)UserStatusEnum.Restricted;

    // Act & Assert
    Should.Throw<DomainException>(() => user.EnsureCanBeUpdated())
        .Message.ShouldBe("User can't be edited - invalid status");
}
  1. Test to see if the method throws a DomainException when the IsRemoved property is true:
[Fact]
public void EnsureCanBeUpdated_IsRemovedIsTrue_ThrowsDomainException()
{
    // Arrange
    var user = new User("sub123", "test@example.com", 1);
    user.IsRemoved = true;

    // Act & Assert
    Should.Throw<DomainException>(() => user.EnsureCanBeUpdated())
        .Message.ShouldBe("User can't be edited - user is removed");
}

In both cases, we use the Should.Throw() method to verify that the method threw the expected exception. We then use the Message.ShouldBe() method to validate the content of the exception message.

Developer: Write a positive test for this method (*He also easily recognizes the type of tests he wants from him*)

Chat: Here is an example of a positive unit test for the EnsureCanBeUpdated() method of the User class:

[Fact]
public void EnsureCanBeUpdated_StatusAndIsRemovedAreFalse_DoesNotThrowException()
{
    // Arrange
    var user = new User("sub123", "test@example.com", 1);

    // Act & Assert
    Should.NotThrow(() => user.EnsureCanBeUpdated());
}

In this test, we check that the EnsureCanBeUpdated() method does not throw an exception when the StatusId and IsRemoved properties have default values ​​(New and false, respectively). We use the Should.NotThrow() method to verify that the method does not throw an exception.

Developer: In the penultimate test you used “user.IsRemoved = true;”. You’d be better off using methods to control class properties if they exist, in this case it’s the SetUserAsRemoved method (*He also suggests to him how he can improve the test if it does not meet my expectations in terms of quality and he notices this error and even justifies my reasoning*)

Chat: You’re right, it’s usually recommended to use public methods to change the state of an object rather than manipulating its properties directly. In this case, to change the value of the IsRemoved property, we should use the SetUserAsRemoved() method. Below is the corrected test:

[Fact]
public void EnsureCanBeUpdated_IsRemovedIsTrue_ThrowsDomainException()
{
    // Arrange
    var user = new User("sub123", "test@example.com", 1);
    user.SetUserAsRemoved();

    // Act & Assert
    Should.Throw<DomainException>(() => user.EnsureCanBeUpdated())
        .Message.ShouldBe("User can't be edited - user is removed");
}

By using the SetUserAsRemoved() method, the test is more readable and explicitly states that we are modifying the IsRemoved property by calling the public method.

Developer: What other good practices should I keep in mind while creating tests?

Chat: Here are some good practices for writing unit tests:

  • Name the tests in a descriptive and understandable way. Tests should be named so that you know what they are testing, for example “ShouldReturnCorrectResultWhenAddingTwoNumbers”. This will make it easier to understand what the test is testing and why it is important.
  • Test individual code units. Unit tests should test single functionalities or methods in isolation from other parts of the code. This way you can ensure that a given code unit works correctly and that problems with other parts of the code do not affect the test results.
  • Use assertions to check expected results. Assertions should be used in unit tests to compare the expected results with the actual results of the method or functionality being tested. This makes it quick and easy to detect if your code is working correctly.
  • Create various test cases. To ensure that your unit tests are comprehensive, you should create different test cases to test different possible scenarios for your code. For example, you can create tests for different edge cases or test different combinations of inputs.
  • Adjust the tests to the level of complexity of the code. Unit tests should be adequate to the complexity of the code. The more complicated the code, the more and more complex tests will be needed. However, the tests should be kept as simple as possible so that they can be easily maintained and changed as the code changes.
  • Use tools and test libraries. There are many test tools and libraries that can help you write and run unit tests. Good testing tools should allow you to easily create tests as well as report and detect problems in your code.
  • Run tests frequently and regularly. Unit tests should be run frequently and regularly to ensure that your code is working correctly. This allows you to quickly detect bugs and problems in your code and quickly fix them before they affect other parts of your system.
  • Test both positive and negative cases. Unit tests should consider both positive and negative cases. Tests

Developer: Will you teach me to write tests, tell me what to do and test me? (*It verifies whether a beginner is able not only to learn everything from him, but also to learn with his help*)

Chat: Of course, I’ll be happy to help you! Below, I’ll give you a quick guide to writing unit tests to help you understand how to write good tests and what to look for.

  • Choose the Right Testing Framework The most popular C# unit testing framework today is xUnit. However, there are other tools such as NUnit or MSTest, so it’s worth exploring each one and choosing the one that best suits your needs.
  • Write a test The test should be short and focus on one specific aspect of the code. It should contain only one assertion that will check if the result of the code under test is as expected.
  • Prepare Test Data Test data should be well prepared and provide full test coverage. Well-written tests should be deterministic, that is, always return the same result for the same input.
  • Use the right tools and libraries To write unit tests in C#, use tools such as FakeItEasy to create dummy objects and libraries such as NUnit and xUnit that contain many useful functions to help you write tests.
  • Run Test Running a test should be easy with just one click. Tests should be automatically run during the CI/CD process to ensure no code goes into production without making sure all tests pass.

If you want to conduct a short test of your knowledge in the field of writing unit tests, ask for a task and I will try to help you!

Developer: Give me an assignment.

Chat: Of course, here’s the job:

Write a C# class that represents a rectangle. The class should have two properties, length and width, as well as methods to calculate the area and perimeter of a rectangle. Then write a set of unit tests for this class using the NUnit library to validate the area calculation method for various inputs.

Developer: It’s a waste of my time, write me this class and I’ll write tests for it (*But I’m too lazy to write boirplate 😉 so I use it*)

Chat: Here is a class that represents a rectangle:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double GetArea()
    {
        return Width * Height;
    }

    public double GetPerimeter()
    {
        return 2 * (Width + Height);
    }
}

This class has two properties: Width and Height, and two methods: GetArea and GetPerimeter, which return the area and perimeter of a rectangle, respectively.

Now you can write unit tests for it in the NUnit library.
Developer:

[Fact]
public void Rectangle_CalculateArea()
{
     var rectangle = new Rectangle() { Height = 2, Width = 5 };
     rectangle.GetArea().ShouldBe(10);
}

(*I wrote him my test, which he verifies and additionally, as a top student, he floods me with tests that I could write*)

Chat: Your unit test looks good! You test the GetArea() method on a rectangle with a height of 2 and a width of 5, and then check if the result is 10. This is a good test to start with.

If you want, you can add some other tests that will test the method for different Width and Height values. For example, you can add tests for Width=0 and Height=0, for negative values, for very large values ​​(e.g. Width=double.MaxValue), for floating point values ​​(e.g. Width=0.5), etc.

Here are some examples of some additional tests:

[Fact]
public void Rectangle_CalculateArea_WidthAndHeightAreZero_ReturnsZero()
{
    // arrange
    var rectangle = new Rectangle() { Height = 0, Width = 0 };

    // act
    var result = rectangle.GetArea();

    // assert
    result.ShouldBe(0);
}

[Fact]
public void Rectangle_CalculateArea_WidthOrHeightIsNegative_ThrowsArgumentException()
{
    // arrange
    var rectangle = new Rectangle() { Height = -2, Width = 5 };

    // act/assert
    Assert.Throws<ArgumentException>(() => rectangle.GetArea());
}

[Fact]
public void Rectangle_CalculateArea_WidthOrHeightIsVeryLarge_ReturnsInfinity()
{
    // arrange
    var rectangle = new Rectangle() { Height = double.MaxValue, Width = double.MaxValue };

    // act
    var result = rectangle.GetArea();

    // assert
    result.ShouldBe(double.PositiveInfinity);
}

[Fact]
public void Rectangle_CalculateArea_WidthOrHeightIsFractional_ReturnsCorrectResult()
{
    // arrange
    var rectangle = new Rectangle() { Height = 1.5, Width = 2.5 };

    // act
    var result = rectangle.GetArea();

    // assert
    result.ShouldBe(3.75);
}

Of course, these are just a few examples, but they show how you can test different scenarios for the GetArea method().

Developer: Let’s go back to my previous User class, you can write me some unit tests for the GetPhoto method()?(*It is worth mentioning here that it remembers the context of the entire conversation, so I can go back to the previous thread I was talking about at any time*)

Chat: Of course, here are sample unit tests for the GetPhoto method():

public class UserTests
{
    [Fact]
    public void GetPhoto_ValidId_ReturnsCorrectPhoto()
    {
        // Arrange
        var photoId = 1;
        var photo = new UserPhoto { Id = photoId, FilePath = "path/to/photo", UserId = 1 };
        var user = new User("sub", "email", 1) { Id = 1 };
        user.Photos.Add(photo);

        // Act
        var result = user.GetPhoto(photoId);

        // Assert
        result.ShouldBe(photo);
    }

    [Fact]
    public void GetPhoto_InvalidId_ThrowsArgumentException()
    {
        // Arrange
        var photoId = 1;
        var user = new User("sub", "email", 1) { Id = 1 };
        
        // Act & Assert
        Should.Throw<ArgumentException>(() => user.GetPhoto(photoId));
    }
}

In the first test, we verify that the GetPhoto() method returns the expected UserPhoto object for the correct photoId.

In the second test, we check if the GetPhoto() method throws an ArgumentException for an invalid photoId.

Developer: You didn’t check the content of the exception (*And again, I point out to him what he forgot, I don’t see anything wrong with it because he doesn’t read my mind, but I’m surprised by the simplicity of how I can ask him to change the code he has already generated. It makes me feel like I’m talking to a real developer. It definitely convinces me that it is a revolutionary solution in my daily work. Because monotonous things that used to take me several dozen minutes, today I am able to do in a few seconds. Just ask him the right question.*)

Chat: Indeed, proper exception validation should include checking the content of the exception. This can be done, for example, with the ShouldThrow<>() method of the Shouldly library, e.g.:

[Fact]
public void GetPhoto_ThrowsException_WhenPhotoNotFound()
{
    // Arrange
    var user = new User("testSub", "testEmail", null);
    
    // Act & Assert
    var ex = Should.Throw<ArgumentException>(() => user.GetPhoto(1));
    ex.Message.ShouldBe("User with Id 0 does not have photo with Id 1");
}

With this test, we make sure that an exception with the expected content is thrown.

There are no comments so far

Leave a Comment

Don't worry. We never use your email for spam

Don't worry. We never use your email for spam