Skip to content

How to mock IKSqlDbContext

Tomas Fabian edited this page Apr 30, 2024 · 6 revisions

The .NET Moq isolation framework is a popular library used for creating and working with mock objects in unit testing scenarios. It provides a simple and intuitive API for defining and configuring mock objects, allowing developers to simulate the dependencies and behavior of external components during testing.

To mock the IKSqlDbContext interface from the ksqlDB.RestApi.Client library using Moq, you can follow these steps:

Install the Moq NuGet package: Make sure you have the Moq package installed in your .NET project. You can install it using the NuGet Package Manager or by running the following command in the Package Manager Console:

Install-Package Moq

The "Extract and Override" unit test pattern is a technique used to test code that has dependencies on external systems or resources. It allows you to isolate the code under test by extracting and overriding dependencies, replacing them with controlled substitutes or mocks.

Within your test setup or configuration, override the actual dependency used by the code under test with your substitute or mock implementation:

using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using ksqlDB.Api.Client.Tests.Helpers;
using ksqlDB.RestApi.Client.KSql.Linq;
using ksqlDB.RestApi.Client.KSql.Linq.PullQueries;
using ksqlDB.RestApi.Client.KSql.Query.Context;
using ksqlDB.RestApi.Client.KSql.RestApi;
using ksqlDB.RestApi.Client.KSql.RestApi.Parameters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Kafka.DotNet.ksqlDB.Tests.Mocking
{
  public class TestableKSqlDBContext : KSqlDBContext
  {
    public TestableKSqlDBContext(string ksqlDbUrl) : base(ksqlDbUrl)
    {
    }

    public TestableKSqlDBContext(KSqlDBContextOptions contextOptions) : base(contextOptions)
    {
    }
    
    public readonly Mock<IKSqlDbProvider> KSqlDbProviderMock = new Mock<IKSqlDbProvider>();

    protected override void OnConfigureServices(IServiceCollection serviceCollection, KSqlDBContextOptions contextOptions)
    {
      serviceCollection.TryAddScoped<IKSqlDbProvider>(c => KSqlDbProviderMock.Object);
      
      base.OnConfigureServices(serviceCollection, contextOptions);
    }
  }
}

Create the mock object: In your unit test, create an instance of the Mock<IKSqlDbContext> class from Moq and assign it to a variable. This will represent your mock object.

Set up mock behavior: Use the Setup method provided by Moq to define the behavior of the methods in the IKSqlDbContext interface that you want to mock. You can specify the return values or configure any other desired behavior.

Verify interactions (optional): After executing the test operations, you can use Moq's verification methods to assert that specific methods on the mock object were called with expected arguments. This helps ensure the correct interaction between your tested code and the IKSqlDbContext mock object.

  [TestClass]
  public class KSqlDbTests
  {
    [TestMethod]
    public async Task GetById()
    {
      //Arrange
      var ksqlDbContextMock = new Mock<IKSqlDBContext>();
      var pullQueryMock = new Mock<IPullable<ElasticSearchEvent>>();
      var pullQueryProviderMock = new Mock<IPullQueryProvider>();

      pullQueryProviderMock.Setup(c => c.CreateQuery<ElasticSearchEvent>(It.IsAny<Expression>()))
        .Returns(pullQueryMock.Object);

      pullQueryMock.Setup(c => c.Provider)
        .Returns(pullQueryProviderMock.Object);

      pullQueryMock.Setup(c => c.Expression)
        .Returns(Expression.Constant(pullQueryMock.Object));

      pullQueryMock.Setup(c => c.GetAsync(It.IsAny<CancellationToken>()))
        .ReturnsAsync(new ElasticSearchEvent { Key = 42 });

      ksqlDbContextMock.Setup(c => c.CreatePullQuery<ElasticSearchEvent>("EventTopic"))
        .Returns(pullQueryMock.Object);

      var classUnderTest = new KSqlDb(ksqlDbContextMock.Object);
      
      //Act
      var elasticSearchEvent = await classUnderTest.GetByIdAsync(42);

      //Assert
      Assert.AreEqual(42, elasticSearchEvent.Key);
    }

    internal static async IAsyncEnumerable<ElasticSearchEvent> ElasticSearchEventsSource()
    {
      yield return new ElasticSearchEvent { Key = 1 };

      yield return new ElasticSearchEvent { Key = 2 };

      await Task.CompletedTask;
    }

    [TestMethod]
    public async Task Subscribe()
    {
      //Arrange
      var ksqlDbContext = new TestableKSqlDBContext(TestParameters.KsqlDBUrl);

      ksqlDbContext.KSqlDbProviderMock
        .Setup(c => c.Run<ElasticSearchEvent>(It.IsAny<QueryStreamParameters>(), It.IsAny<CancellationToken>()))
        .Returns(ElasticSearchEventsSource);
      
      var classUnderTest = new KSqlDb(ksqlDbContext);
      
      var semaphoreSlim = new SemaphoreSlim(0, 1);
      var receivedValues = new List<ElasticSearchEvent>();

      //Act
      var qbservable = classUnderTest.CreateElasticSearchEventQuery();

      var subscription = qbservable.Subscribe(value =>
      {
        receivedValues.Add(value);
      }, exception =>
      {
        semaphoreSlim.Release();
      }, () => semaphoreSlim.Release());

      await semaphoreSlim.WaitAsync();

      //Assert
      Assert.AreEqual(2, receivedValues.Count);

      using(subscription){}
    }
  }

By following these steps, you can create a mock object of IKSqlDbContext or any other related interfaces using Moq and define the desired behavior for your unit tests. This allows you to isolate your code under test and simulate interactions with the ksqlDB.RestApi.Client library.

The class being tested is implemented with the following code:

  public interface IKSqlDb
  {
    Task<ElasticSearchEvent> GetByIdAsync(int id);
    IQbservable<ElasticSearchEvent> CreateElasticSearchEventQuery();
  }

  class KSqlDb : IKSqlDb
  {
    private readonly IKSqlDBContext context;

    public KSqlDb(IKSqlDBContext context)
    {
      this.context = context;
    }

    public async Task<ElasticSearchEvent> GetByIdAsync(int id)
    {
      var response = await context.CreatePullQuery<ElasticSearchEvent>("EventTopic")
        .Where(c => c.Key == id)
        .GetAsync();

      return response;
    }

    public IQbservable<ElasticSearchEvent> CreateElasticSearchEventQuery()
    {
      var query = context.CreatePushQuery<ElasticSearchEvent>()
        .Where(p => p.Key != 33);

      return query;
    }
  }

  public class ElasticSearchEvent
  {
    public int Key { get; set; }
  }

Congratulations on your testing endeavor! I wish you a successful and fruitful testing process. May your tests be comprehensive, your results insightful, and your software robust.

Happy testing!