elastic / apm-agent-dotnet

https://www.elastic.co/guide/en/apm/agent/dotnet/current/index.html
Apache License 2.0
586 stars 208 forks source link

Mock Apm Server for ASP.NET Core #432

Closed ertgamgam closed 5 years ago

ertgamgam commented 5 years ago

I have a code block like this:

var span = Agent.Tracer.CurrentTransaction.StartSpan(apmSpanName, ApiConstants.TypeRequest, ApiConstants.SubtypeHttp); try { //do something... } catch (Exception e) { span.CaptureException(e); throw; } finally { span.End(); }

How can i mock this code block for unit testing? Thanks

SergeyKleyman commented 5 years ago

Hi @ertgamgam. First of all thank you for trying out the agent.

Second - a small administrative issue. As mentioned in README we recommend using the forum for questions and GitHub tickets for confirmed bugs and enhancement requests. The forum allows other users to learn from the questions and answers and the number of people that can answer a question at the forum is much larger than at the Github repo which is mostly for developers of the agent.

Regarding your question - the simplest way to allow mocking the agent is by making your code dependent on an instance of ITracer, an interface of which Agent.Tracer is an instance, instead accessing Agent.Tracer singleton directly. You can make an instance of ITracer available to your code using dependency injection or any other approach you prefer. Now with your code depending only on ITracer and not on Agent.Tracer, implementing a mock tracer becomes a very simple task. For example you can implement a no-op mock tracer by returning null from every method in the interface (regarding returning null please see the next paragraph).

I would advise using ITransaction and ISpan instances returned by the tracer more defensively because they can be null for various reasons (agent is disabled, etc.). So your code snippet will become

// Obtain ITracer dependency - no-op mock for testing, `Agent.Tracer` in production, etc.
ITracer apmAgentTracer = ...;

// ...

var span = apmAgentTracer.CurrentTransaction?.StartSpan(apmSpanName, ApiConstants.TypeRequest, ApiConstants.SubtypeHttp);
try
{
    //do something...
}
catch (Exception e)
{
    span?.CaptureException(e);
    throw;
}
finally
{
    span?.End();
}
SergeyKleyman commented 5 years ago

Please let us know if the above answers your question and if the issue can be closed.

SergeyKleyman commented 5 years ago

@ertgamgam I'm going to soft-close this issue - please re-open it if not all the concerns were addressed.

kduenke commented 4 years ago

For anyone who finds this thread, you can also do something like this.

I'm using constructor injection and in my unit test I just call whatever function was passed into ITracer (i.e. please just execute the function I told you to).

When the functions inside the Returns are called they receive null as the value of both ITransaction and ISpan, so be sure to use a null conditional (e.g. transaction?., span?.) in your function implementation.

Unit test setup:

var tracer = new Mock<ITracer>();

// CaptureTransaction
tracer.Setup(o => o.CaptureTransaction(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Func<ITransaction, Task>>(), It.IsAny<DistributedTracingData>()))
   .Returns<string, string, Func<ITransaction, Task>, DistributedTracingData>((name, type, func, distributedTracingData) => func(null));

// CurrentTransaction.CaptureSpan
tracer.Setup(o => o.CurrentTransaction.CaptureSpan(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Action<ISpan>>(), It.IsAny<string>(), It.IsAny<string>()))
   .Callback((string name, string type, Action<ISpan> capturedAction, string subType, string action) => capturedAction(null));

Example implementations:

await _tracer.CaptureTransaction(
   name: "GET /api/values",
   type: ApiConstants.TypeRequest,
   func: async (transaction) =>
   {
      transaction?.Labels.Add(nameof(mySpecialVariable), mySpecialVariable);
   });

await _tracer.CurrentTransaction.CaptureSpan(
   name: "MySpecialName",
   type: ApiConstants.ActionExec,
   capturedAction: async (span) =>
   {
      span?.Labels.Add(nameof(mySpecialVariable), mySpecialVariable);
   });
tinonetic commented 1 year ago

Hi @ertgamgam. First of all thank you for trying out the agent.

Second - a small administrative issue. As mentioned in README we recommend using the forum for questions and GitHub tickets for confirmed bugs and enhancement requests. The forum allows other users to learn from the questions and answers and the number of people that can answer a question at the forum is much larger than at the Github repo which is mostly for developers of the agent.

Regarding your question - the simplest way to allow mocking the agent is by making your code dependent on an instance of ITracer, an interface of which Agent.Tracer is an instance, instead accessing Agent.Tracer singleton directly. You can make an instance of ITracer available to your code using dependency injection or any other approach you prefer. Now with your code depending only on ITracer and not on Agent.Tracer, implementing a mock tracer becomes a very simple task. For example you can implement a no-op mock tracer by returning null from every method in the interface (regarding returning null please see the next paragraph).

I would advise using ITransaction and ISpan instances returned by the tracer more defensively because they can be null for various reasons (agent is disabled, etc.). So your code snippet will become

// Obtain ITracer dependency - no-op mock for testing, `Agent.Tracer` in production, etc.
ITracer apmAgentTracer = ...;

// ...

var span = apmAgentTracer.CurrentTransaction?.StartSpan(apmSpanName, ApiConstants.TypeRequest, ApiConstants.SubtypeHttp);
try
{
    //do something...
}
catch (Exception e)
{
    span?.CaptureException(e);
    throw;
}
finally
{
    span?.End();
}

How would I instantiate this ITracer object?

In both the unit test and the concrete usage?

I'm using ASP.NET

Is there a sample app I can reference?