简体   繁体   中英

Unit test RabbitMQ push with C# - .Net Core

I have created a .net core API, which pushes a message in RabbitMQ queue. I have used IOptions to read configuration data from .json file and added it as dependency.

Below is the code of my controller:

[Route("api/[controller]")]
public class RestController : Controller
{
    private RabbitMQConnectionDetail _connectionDetail;

    public RestController(IOptions<RabbitMQConnectionDetail> connectionDetail)
    {
        _connectionDetail = connectionDetail.Value;
    }

    [HttpPost]
    public IActionResult Push([FromBody] OrderItem orderItem)
    {
        try
        {
            using (var rabbitMQConnection = new RabbitMQConnection(_connectionDetail.HostName,
                _connectionDetail.UserName, _connectionDetail.Password))
            {
                using (var connection = rabbitMQConnection.CreateConnection())
                {
                    var model = connection.CreateModel();
                    var helper = new RabbitMQHelper(model, "Topic_Exchange");
                    helper.PushMessageIntoQueue(orderItem.Serialize(), "Order_Queue");
                }
            }
        }
        catch (Exception)
        {
            return StatusCode((int)HttpStatusCode.BadRequest);
        }
        return Ok();
    }
 }

Connection details class has the below properties

public class RabbitMQConnectionDetail
{
    public string HostName { get; set; }

    public string UserName { get; set; }

    public string Password { get; set; }
}

Now I want to unit test it, but since I am going to test it against a blackbox, I'm not able to think of how to unit test it and looking for kind help.

ConnectionClass

public class RabbitMQConnection : IDisposable
{   
    private static IConnection _connection;
    private readonly string _hostName;
    private readonly string _userName;
    private readonly string _password;

    public RabbitMQConnection(string hostName, string userName, string password)
    {
        _hostName = hostName;
        _userName = userName;
        _password = password;
    }

    public IConnection CreateConnection()
    {
        var _factory = new ConnectionFactory
        {
            HostName = _hostName,
            UserName = _userName,
            Password = _password
        };
        _connection = _factory.CreateConnection();
        var model = _connection.CreateModel();

        return _connection;
    }

    public void Close()
    {
        _connection.Close();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _connection.Close();
        }
    }

    ~ RabbitMQConnection()
    {
        Dispose(false);
    }
}

Helper class

public class RabbitMQHelper
{
    private static IModel _model;
    private static string _exchangeName;
    const string RoutingKey = "dummy-key.";

    public RabbitMQHelper(IModel model, string exchangeName)
    {
        _model = model;
        _exchangeName = exchangeName;
    }


    public void SetupQueue(string queueName)
    {
        _model.ExchangeDeclare(_exchangeName, ExchangeType.Topic);
        _model.QueueDeclare(queueName, true, false, false, null);
        _model.QueueBind(queueName, _exchangeName, RoutingKey);
    }

    public void PushMessageIntoQueue(byte[] message, string queue)
    {
        SetupQueue(queue);
        _model.BasicPublish(_exchangeName, RoutingKey, null, message);
    }

    public byte[] ReadMessageFromQueue(string queueName)
    {
        SetupQueue(queueName);
        byte[] message;
        var data = _model.BasicGet(queueName, false);
        message = data.Body;
        _model.BasicAck(data.DeliveryTag, false);
        return message;
    }
}

Tightly coupling your Controller to implementation concerns are making it difficult to test your controller without side-effects. From the sample you provided you have shown that you are encapsulating the 3rd part API implementations and only exposing abstractions. Good. You however have not created an abstraction that would allow you to mock them when testing. I suggest a refactor of the RabbitMQConnection to allow for this.

First have your own backing abstraction.

public interface IRabbitMQConnectionFactory {
    IConnection CreateConnection();
}

And refactor RabbitMQConnection as follows

public class RabbitMQConnection : IRabbitMQConnectionFactory {
    private readonly RabbitMQConnectionDetail connectionDetails;

    public RabbitMQConnection(IOptions<RabbitMQConnectionDetail> connectionDetails) {
        this.connectionDetails = connectionDetails.Value;
    }

    public IConnection CreateConnection() {
        var factory = new ConnectionFactory {
            HostName = connectionDetails.HostName,
            UserName = connectionDetails.UserName,
            Password = connectionDetails.Password
        };
        var connection = factory.CreateConnection();
        return connection;
    }
}

Take some time and review exactly what was done with this refactor. The IOptions was moved from the Controller to the factory and the RabbitMQConnection has also been simplified to do it's intended purpose. Creating a connection.

The Controller now would need to be refactored as well

[Route("api/[controller]")]
public class RestController : Controller {
    private readonly IRabbitMQConnectionFactory factory;

    public RestController(IRabbitMQConnectionFactory factory) {
        this.factory = factory;
    }

    [HttpPost]
    public IActionResult Push([FromBody] OrderItem orderItem) {
        try {                
            using (var connection = factory.CreateConnection()) {
                var model = connection.CreateModel();
                var helper = new RabbitMQHelper(model, "Topic_Exchange");
                helper.PushMessageIntoQueue(orderItem.Serialize(), "Order_Queue");
                return Ok();
            }
        } catch (Exception) {
            //TODO: Log error message
            return StatusCode((int)HttpStatusCode.BadRequest);
        }
    }
}

Again note the simplification of the controller. This now allows the factory to be mocked and injected when testing and by extension allows the mocks to be used by the RabbitMQHelper . You can use your mocking framework of choice for dependencies or pure DI.

I dont think it is a unit test scenario. If you want to to test with external component ie database or message queue then i suggest you do it as integration test.

What we do is to have a sand box environment with component SQL database and azure message bus. We have code to correctly set the state for this component ie seed the database and clear the message bus. Then we run test on the environment and check the state of the database or message bus count etc.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM