3
\$\begingroup\$

I find it hard to do integration tests for a repository and very time consuming.

Things I think about when creating integration tests for a repository: Should I skip them? Should I create an in memory database? Should I create a local database for the test? Should I delete inserted data? Is my repository caching results, should I use another instance to verify results?

I implemented this repository with Dapper as it will be the only repository in my solution and Dapper is so light weight. I am a big fan of NHibernate but I felt it was an overkill.

I would be happy for any comments on how my tests could be better :)

Here are my tests

[TestFixture]
public class NotificationRepositoryTests
{
    private IFixture fixture;
    private TestNotificationRepository notificationRepository;

    [SetUp]
    public void SetUp()
    {
        fixture = new Fixture();
        fixture.Customize<Notification>(c => c.With(x => x.Processed, CreateSqlServerFriendlyDate()));
        notificationRepository = new TestNotificationRepository();
    }

    [TearDown]
    public void TearDown()
    {
        notificationRepository.DeleteInserted();
    }

    [Test]
    public void InsertShouldReturnCreateAnId()
    {
        var notification = fixture.Build<Notification>().Without(x => x.Id).Create();

        notificationRepository.Insert(notification);

        Assert.That(notification.Id, Is.Not.EqualTo(0));
    }

    [Test]
    public void InsertShouldInsertNotification()
    {
        var notification = fixture.Create<Notification>();

        notificationRepository.Insert(notification);

        var result = notificationRepository.Get(notification.Id);
        notification.AsSource().OfLikeness<Notification>().ShouldEqual(result);
    }

    [Test]
    public void UpdateShouldUpdateNotification()
    {
        //Arrange
        var notification = fixture.Create<Notification>();
        notificationRepository.Insert(notification);

        var notificationUpdate = fixture.Build<Notification>().With(x => x.Processed, CreateSqlServerFriendlyDate()).Create();
        notification.DocumentId = notificationUpdate.DocumentId;
        notification.DocumentName = notificationUpdate.DocumentName;
        notification.Notes = notificationUpdate.Notes;
        notification.Status = notificationUpdate.Status;
        notification.Processed = notificationUpdate.Processed;

        //Act
        notificationRepository.Update(notification);

        //Assert
        var result = notificationRepository.Get(notification.Id);
        notification.AsSource().OfLikeness<Notification>().ShouldEqual(result);
    }

    [Test]
    [TestCase("King")]
    [TestCase("Lord of the rings")]
    public void GetUnprocessedLikeShouldReturnNotificaitonWithPrefixAndWithoutProcessingDate(string metadataPrefix)
    {
        //Arrange
        var notification = fixture.Create<Notification>();
        notification.Metadata = metadataPrefix + notification.Metadata;
        notification.Processed = null;

        notificationRepository.Insert(notification);

        //Act
        var results = notificationRepository.GetUnprocessedLike(metadataPrefix);

        //Assert
        Assert.IsTrue(results.Any(x => notification.AsSource().OfLikeness<Notification>().Equals(x)));
    }

    [Test]
    [TestCase("Pizza")]
    [TestCase("Hamburger")]
    public void GetUnprocessedLikeShouldNotReturnNotificaitonWithPrefixAndWithProcessingDate(string metadataPrefix)
    {
        //Arrange
        var notification = fixture.Create<Notification>();
        notification.Metadata = metadataPrefix + notification.Metadata;
        notification.Processed = CreateSqlServerFriendlyDate();

        notificationRepository.Insert(notification);

        //Act
        var results = notificationRepository.GetUnprocessedLike(metadataPrefix);

        //Assert
        Assert.IsFalse(results.Any(x => notification.AsSource().OfLikeness<Notification>().Equals(x)));
    }

    [Test]
    [TestCase("Druid")]
    [TestCase("Mage")]
    public void GetUnprocessedLikeShouldNotReturnNotificaitonWithoutPrefixAndWithoutProcessingDate(string metadataPrefix)
    {
        //Arrange
        var notification = fixture.Create<Notification>();
        notification.Metadata = "No prefix" + notification.Metadata;
        notification.Processed = null;

        notificationRepository.Insert(notification);

        //Act
        var results = notificationRepository.GetUnprocessedLike(metadataPrefix);

        //Assert
        Assert.IsFalse(results.Any(x => notification.AsSource().OfLikeness<Notification>().Equals(x)));
    }

    /// <summary>
    /// Sql server does not have the same precision as .Net so we don't create dates with milliseconds in theses tests
    /// </summary>
    private DateTime CreateSqlServerFriendlyDate()
    {
        var preciseDate = fixture.Create<DateTime>();
        return new DateTime(preciseDate.Year, preciseDate.Month, preciseDate.Day,
            preciseDate.Hour, preciseDate.Minute, preciseDate.Second);
    }
}

public class TestNotificationRepository : INotificationRepository
{
    private readonly string TestConnectionString = "Server=devserver;Database=devdatabase;Trusted_Connection=True;";

    private readonly NotificationRepository notificationRepository;
    private readonly HashSet<Notification> insertedNotifications;

    public TestNotificationRepository()
    {
        this.notificationRepository = new NotificationRepository(TestConnectionString);
        insertedNotifications = new HashSet<Notification>();
    }

    public Notification Get(int id)
    {
        return notificationRepository.Get(id);
    }

    public void Insert(Notification notification)
    {
        notificationRepository.Insert(notification);
        insertedNotifications.Add(notification);
    }

    public void Update(Notification notification)
    {
        notificationRepository.Update(notification);
    }

    public IList<Notification> GetUnprocessedLike(string metadata)
    {
        return notificationRepository.GetUnprocessedLike(metadata);
    }

    public void DeleteInserted()
    {
        foreach (var notification in insertedNotifications)
        {
            notificationRepository.Delete(notification);
        }
    }
}

Repository implemented with Dapper

public class NotificationRepository : INotificationRepository
{
    private readonly string connectionString;

    public NotificationRepository(string connectionString)
    {
        if (connectionString == null) throw new ArgumentNullException("connectionString");
        this.connectionString = connectionString;
    }

    public Notification Get(int id)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            return connection.Query<Notification>(
                "Select Id, DocumentId, DocumentName, Notes, Metadata, Status, Processed From Notification " +
                "Where Id = @id", new{id}).Single();
        }
    }

    public void Insert(Notification notification)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            var result = connection.Query<int>("Insert into Notification " +
            "(DocumentId, DocumentName, Notes, Metadata, Status, Processed) Values " + 
            "(@DocumentId, @DocumentName, @Notes, @Metadata, @Status, @Processed); " +
            "SELECT CAST(SCOPE_IDENTITY() as int);", notification);

            notification.Id = result.Single();
        }
    }

    public void Update(Notification notification)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Execute(
                "Update Notification set " +
                "DocumentName = @DocumentName, Notes = @Notes, " +
                "Metadata = @Metadata, Status = @Status, Processed = @Processed, DocumentId = @DocumentId " +
                "Where Id = @Id", notification);
        }
    }

    public void Delete(Notification notification)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Execute("Delete From Notification Where Id = @Id", notification);
        }
    }

    public IList<Notification> GetUnprocessedLike(string metadata)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            return connection.Query<Notification>(
                "Select Id, DocumentId, DocumentName, Notes, Metadata, Status, Processed From Notification " +
                "Where Metadata like @metadata + '%' and Processed is null", new{metadata})
                .ToList();
        }
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ You absolutely should test this! It's not a unit test though, it's an integration test. They're every bit as important as unit tests, they're just less of them and we run them less often. \$\endgroup\$
    – RubberDuck
    Commented Nov 27, 2015 at 0:15
  • \$\begingroup\$ Everything its looking good for me except delete or update integration test and second you are not using constant and resource messages better to put your message in resourcemessage or query in constant.should we create one dummy object in db which always will be available , why i am saying that because when your delete perfomation will performe first it will be delete that record but when the second time will you run it so it will be fail so better to create one dummy object in db . \$\endgroup\$ Commented Nov 30, 2015 at 13:55

1 Answer 1

3
\$\begingroup\$

Overall

On the whole I like your approach and your tests look pretty robust. I have tended to have simple 'does this execute' type integration tests at a repository level that are manually validated, since they only really need to be run if you're updating the repository or the underlying database. I then rely on higher level use case integration tests, where the repositories are used under the hood, to flag up any data discrepancies. Your approach of having the tests cleanup after themselves is a nice tidy approach.

Consider Categories

It looks like you're using NUnit and if you are, then the one thing I would suggest you do though is mark your different types of tests using categories. I leave most unit tests without categories, however I mark integration tests with categories so that they can easily be excluded / included into builds. You can use the attribute at either a 'Test' or a 'TestFixture' level.

[Category("DBIntegrationTest")]

If you're running tests from within Visual Studio, then Categories are interpreted as 'Traits', so again you can easily exclude them from the tests that you're running, or just run the specific category/trait. NUnit itself and the console runner support multiple category attributes (so you can mark all tests 'IntegrationTests' and mark db tests 'DBIntegrationTests' then run subsets), however not all test runners recognise multiple categories (the default VS runner seems to just pick the first one).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.