Compartir a través de


Creación de un marco de pruebas

En este artículo se explica cómo crear un marco de pruebas personalizado para Microsoft.Testing.Platform. El marco de pruebas es la única extensión obligatoria. Detecta y ejecuta pruebas e informa de los resultados de nuevo a la plataforma.

Para obtener el resumen completo del punto de extensión y los conceptos dentro del proceso o fuera del proceso, consulte Creación de extensiones personalizadas.

Si va a migrar un marco de pruebas basado en VSTest existente, la implementación de la interfaz de forma nativa es el enfoque recomendado. La extensión VSTest Bridge está disponible como paso de transición, pero una implementación nativa proporciona la mejor experiencia.

Extensión de marco de pruebas

El marco de pruebas es la extensión principal que proporciona a la plataforma de pruebas la capacidad de detectar y ejecutar pruebas. El marco de pruebas es el responsable de comunicar los resultados de las pruebas a la plataforma de pruebas. El marco de pruebas es la única extensión obligatoria necesaria para ejecutar una sesión de prueba.

Registro de un marco de pruebas

En esta sección se explica cómo registrar el marco de pruebas con la plataforma de pruebas. Solo se registra un framework de pruebas por generador de aplicaciones de prueba mediante la API, como se muestra en la documentación de arquitectura de Microsoft.Testing.Platform.

La API de registro se define de la siguiente manera:

ITestApplicationBuilder RegisterTestFramework(
    Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
    Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);

La API espera dos fábricas:

  1. : se trata de un delegado que acepta un objeto que implementa la interfaz y devuelve un objeto que implementa la interfaz . proporciona acceso a servicios de plataforma como configuraciones, registradores y argumentos de línea de comandos.

    La interfaz se usa para anunciar las funcionalidades admitidas por el marco de pruebas para la plataforma y las extensiones. Permite que la plataforma y las extensiones interactúen correctamente mediante la implementación y compatibilidad de comportamientos específicos. Para comprender mejor el concepto de funcionalidades, consulte la sección correspondiente.

  2. : se trata de un delegado que toma un objeto ITestFrameworkCapabilities, que es la instancia devuelta por y un IServiceProvider para proporcionar acceso a los servicios de plataforma una vez más. El objeto devuelto esperado es uno que implementa la interfaz ITestFramework. actúa como motor de ejecución que detecta y ejecuta pruebas y, a continuación, comunica los resultados a la plataforma de pruebas.

La necesidad de que la plataforma separe la creación de y la creación de ITestFramework es una optimización para evitar la creación del marco de pruebas si las funcionalidades admitidas no son suficientes para ejecutar la sesión de prueba actual.

Considere el siguiente ejemplo de código de usuario, que muestra un registro de marco de pruebas que devuelve un conjunto de capacidades vacío:

internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities
{
    public IReadOnlyCollection<ITestFrameworkCapability> Capabilities => [];
}

internal class TestingFramework : ITestFramework
{
   public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider)
   {
       // ...
   }
   // Omitted for brevity...
}

public static class TestingFrameworkExtensions
{
    public static void AddTestingFramework(this ITestApplicationBuilder builder)
    {
        builder.RegisterTestFramework(
            _ => new TestingFrameworkCapabilities(),
            (capabilities, serviceProvider) => new TestingFramework(capabilities, serviceProvider));
    }
}

// ...

Ahora, considere el punto de entrada correspondiente de este ejemplo con el código de registro:

var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
// Register the testing framework
testApplicationBuilder.AddTestingFramework();
using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();

Nota:

La devolución de ITestFrameworkCapabilities vacío no debe impedir la ejecución de la sesión de prueba. Todos los marcos de pruebas deben ser capaces de detectar y ejecutar pruebas. El impacto debe limitarse a las extensiones que pueden optar por no participar si el marco de pruebas carece de una determinada característica.

Creación de un marco de pruebas

se implementa mediante extensiones que proporcionan un marco de pruebas:

public interface ITestFramework : IExtension
{
    Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context);
    Task ExecuteRequestAsync(ExecuteRequestContext context);
    Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context);
}

Interfaz

La interfaz hereda de la interfaz , que es una interfaz de la que heredan todos los puntos de extensión. se usa para recuperar el nombre y la descripción de la extensión. también proporciona una manera de habilitar o deshabilitar dinámicamente la extensión en la configuración, a través de . Asegúrese de devolver de este método si no tiene requisitos específicos para deshabilitar esta funcionalidad.

El método

Se llama al método al principio de la sesión de prueba y se usa para inicializar el marco de pruebas. La API acepta un objeto y devuelve .

public sealed class CreateTestSessionContext : TestSessionContext
{
    public CancellationToken CancellationToken { get; }
}

La propiedad se hereda de (consulte la sección TestSessionContext). se usa para detener la ejecución de .

El objeto que se devuelve es

public sealed class CreateTestSessionResult
{
    public string? WarningMessage { get; set; }
    public string? ErrorMessage { get; set; }
    public bool IsSuccess { get; set; }
}

La propiedad se usa para indicar si la creación de la sesión se ha realizado correctamente. Cuando devuelve , se detiene la ejecución de la prueba.

El método

El método se yuxtapone a en funcionalidad, con la única diferencia de los nombres de los objetos. Para obtener más información, vea la sección .

El método

El método acepta un objeto de tipo . Este objeto, como sugiere su nombre, contiene los datos específicos sobre la acción que se espera que realice el marco de pruebas. La definición de es:

public sealed class ExecuteRequestContext
{
    public IRequest Request { get; }
    public IMessageBus MessageBus { get; }
    public CancellationToken CancellationToken { get; }
    public void Complete();
}

: esta es la interfaz base de cualquier tipo de solicitud. Debe pensar en el marco de pruebas como un servidor con estado en proceso en el que el ciclo de vida es:

Diagrama de secuencia que representa el ciclo de vida del marco de pruebas.

El diagrama anterior ilustra que la plataforma de pruebas emite tres solicitudes después de crear la instancia del marco de pruebas. El marco de pruebas procesa estas solicitudes y utiliza el servicio , que se incluye en la propia solicitud, para entregar el resultado de cada solicitud específica. Una vez que se ha controlado una solicitud determinada, el marco de pruebas invoca el método en él, lo que indica a la plataforma de pruebas que se ha cumplido la solicitud. La plataforma de pruebas supervisa todas las solicitudes enviadas. Una vez completadas todas las solicitudes, invoca y elimina la instancia (si se implementa ). Es evidente que las solicitudes y sus finalizaciones pueden superponerse, lo que permite la ejecución simultánea y asincrónica de las solicitudes.

Nota:

Actualmente, la plataforma de pruebas no envía solicitudes superpuestas y espera la finalización de una solicitud antes de enviar la siguiente. Sin embargo, este comportamiento puede cambiar en el futuro. La compatibilidad con solicitudes simultáneas se determinará a través del sistema de capacidades.

La implementación especifica la solicitud precisa que debe cumplirse. El marco de pruebas identifica el tipo de solicitud y lo controla en consecuencia. Si no se reconoce el tipo de solicitud, se debe generar una excepción.

Puede encontrar detalles sobre las solicitudes disponibles en la sección IRequest.

: este servicio, vinculado a la solicitud, permite que el marco de pruebas publique de forma asincrónica información sobre la solicitud en curso en la plataforma de pruebas. El bus de mensajes actúa como concentrador central de la plataforma, lo que facilita la comunicación asincrónica entre todos los componentes y extensiones de la plataforma. Para obtener una lista completa de la información que se puede publicar en la plataforma de pruebas, consulte la sección IMessageBus.

: este token se utiliza para interrumpir el procesamiento de una solicitud determinada.

: como se muestra en la secuencia anterior, el método notifica a la plataforma que la solicitud se ha procesado correctamente y toda la información pertinente se ha transmitido al IMessageBus.

Advertencia

Si no se invoca en la solicitud, la aplicación de prueba dejará de responder.

Para personalizar el marco de pruebas según sus requisitos o los de los usuarios, puede usar una sección personalizada dentro del archivo de configuración o con opciones de línea de comandos personalizadas.

Gestión de solicitudes

En la sección siguiente proporciona una descripción detallada de las distintas solicitudes que puede recibir y procesar un marco de pruebas.

Antes de continuar con la siguiente sección, es fundamental comprender exhaustivamente el concepto de IMessageBus, que es el servicio esencial para transmitir información de ejecución de pruebas a la plataforma de pruebas.

TestSessionContext

es una propiedad compartida en todas las solicitudes y proporciona información sobre la sesión de prueba en curso:

public class TestSessionContext
{
    public SessionUid SessionUid { get; }
}

public readonly struct SessionUid(string value)
{
    public string Value { get; }
}

consta de , un identificador único para la sesión de prueba en curso que ayuda a registrar y correlacionar los datos de la sesión de prueba.

SolicitudDeEjecuciónDePruebaDiscover

public class DiscoverTestExecutionRequest
{
    public TestSessionContext Session { get; }
    public ITestExecutionFilter Filter { get; }
}

indica al marco de pruebas que detecte las pruebas y comunique esta información a través de IMessageBus.

Como se describe en la sección anterior, la propiedad de una prueba detectada es . Esto es un fragmento de código genérico a modo de referencia:

var testNode = new TestNode
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        DiscoveredTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        discoverTestExecutionRequest.Session.SessionUid,
        testNode));

// ...

EjecutarSolicitudDeEjecuciónDePrueba

public class RunTestExecutionRequest
{
    public TestSessionContext Session { get; }
    public ITestExecutionFilter Filter { get; }
}

indica al marco de pruebas que ejecute las pruebas y comunique esta información al IMessageBus.

Esto es un fragmento de código genérico a modo de referencia:

var skippedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        SkippedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        skippedTestNode));

// ...

var successfulTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        PassedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        successfulTestNode));

// ...

var assertionFailedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new FailedTestNodeStateProperty(assertionException)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        assertionFailedTestNode));

// ...

var failedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new ErrorTestNodeStateProperty(ex.InnerException!)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        failedTestNode));

Los datos

Como se ha mencionado en la sección IMessageBus, antes de usar el bus de mensajes, debe especificar el tipo de datos que desea proporcionar. La plataforma de pruebas ha definido un tipo conocido, , para representar el concepto de información de actualización de prueba.

En esta parte del documento se explica cómo se usan estos datos de carga. Analicemos la superficie:

public sealed class TestNodeUpdateMessage(
    SessionUid sessionUid,
    TestNode testNode,
    TestNodeUid? parentTestNodeUid = null)
{
    public TestNode TestNode { get; }
    public TestNodeUid? ParentTestNodeUid { get; }
}

public class TestNode
{
    public required TestNodeUid Uid { get; init; }
    public required string DisplayName { get; init; }
    public PropertyBag Properties { get; init; } = new();
}

public sealed class TestNodeUid(string value);

public sealed partial class PropertyBag
{
    public PropertyBag();
    public PropertyBag(params IProperty[] properties);
    public PropertyBag(IEnumerable<IProperty> properties);
    public int Count { get; }
    public void Add(IProperty property);
    public bool Any<TProperty>();
    public TProperty? SingleOrDefault<TProperty>();
    public TProperty Single<TProperty>();
    public TProperty[] OfType<TProperty>();
    public IEnumerable<IProperty> AsEnumerable();
    public IEnumerator<IProperty> GetEnumerator();
    ...
}

public interface IProperty
{
}
  • : consta de dos propiedades: y . indica que una prueba puede tener una prueba principal al introducir el concepto del árbol de pruebas donde s pueden ordenarse en relación unas con otras. Esta estructura permite futuras mejoras y características basadas en la relación de árbol entre los nodos. Si el marco de pruebas no requiere una estructura de árbol de prueba, puede optar por no usarla y simplemente establecerla en nula, lo que dará como resultado una lista plana directa de s.

  • : consta de tres propiedades, una de las cuales es de tipo . Esto sirve de ID ESTABLE ÚNICO para el nodo. El término ID ESTABLE ÚNICO implica que el mismo debe mantener un IDÉNTICO en diferentes ejecuciones y sistemas operativos. es una cadena opaca arbitraria que la plataforma de prueba acepta tal como es.

Importante

La estabilidad y la unicidad del identificador son cruciales en el dominio de las pruebas. Permiten la selección precisa de una única prueba para su ejecución y permiten que el ID sirva como identificador persistente de una prueba, lo que facilita potentes ampliaciones y funciones.

La segunda propiedad es , que es el nombre amigable de la prueba. Por ejemplo, este nombre se muestra al ejecutar la línea de comandos .

El tercer atributo es , que es un tipo . Como se muestra en el código, se trata de un contenedor de propiedades especializado que contiene propiedades genéricas sobre Esto implica que puede anexar cualquier propiedad al nodo que implementa la interfaz de marcador de posición .

La plataforma de pruebas identifica propiedades específicas agregadas a para determinar si se ha superado, no se ha realizado correctamente o se ha omitido una prueba.

Puede encontrar la lista actual de propiedades disponibles junto con su descripción correspondiente en la sección TestNodeUpdateMessage.TestNode.

Normalmente, el tipo es accesible en cada y se utiliza para almacenar propiedades diversas que la plataforma y las extensiones pueden consultar. Este mecanismo nos permite mejorar la plataforma con nueva información sin introducir cambios importantes. Si un componente reconoce la propiedad, puede consultarla; en caso contrario, la ignorará.

Por último, esta sección aclara que la implementación de su framework de pruebas debe implementar que genera s como en el ejemplo siguiente:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage)
   ];

   // ...
}

Si el adaptador de prueba requiere la publicación de archivos durante la ejecución, puede encontrar las propiedades reconocidas en este archivo de origen: . Como se observa, puede proporcionar activos de archivo de forma general o asociarlos a un específico. Recuerde que si piensa insertar un , debe declararlo con antelación en la plataforma, como se muestra a continuación:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage),
       typeof(SessionFileArtifact)
   ];

   // ...
}

Propiedades conocidas

Como se detalla en la sección de solicitudes, la plataforma de pruebas identifica las propiedades específicas agregadas a para determinar el estado de un (por ejemplo, exitoso, fallido, omitido, etc.). Esto permite que el tiempo de ejecución muestre con precisión una lista de pruebas erróneas con su información correspondiente en la consola, y que establezca el código de salida apropiado para el proceso de prueba.

En este segmento, dilucidaremos las diversas opciones bien conocidas y sus respectivas implicaciones.

Para obtener una lista completa de las propiedades conocidas, consulte TestNodeProperties.cs. Si observa que falta una descripción de la propiedad, presente un problema.

Estas propiedades se pueden dividir en las siguientes categorías:

  1. Información genérica: propiedades que se pueden incluir en cualquier tipo de solicitud.
  2. Información de detección: propiedades proporcionadas durante una solicitud de detección .
  3. Información de ejecución: propiedades proporcionadas durante una solicitud de ejecución de prueba .

Algunas propiedades son obligatorias, mientras que otras son opcionales. Las propiedades obligatorias son necesarias para proporcionar funcionalidad de prueba básica, como notificar pruebas con errores e indicar si toda la sesión de prueba se ha realizado correctamente o no.

Las propiedades opcionales, por su parte, mejoran la experiencia de la prueba al proporcionar información adicional. Son especialmente útiles en escenarios IDE (como VS, VSCode, etc.), ejecuciones de consola o cuando se da soporte a extensiones específicas que requieren información más detallada para funcionar correctamente. Sin embargo, estas propiedades opcionales no afectan a la ejecución de las pruebas.

Nota:

Las extensiones se encargan de alertar y administrar excepciones cuando necesitan información específica para funcionar correctamente. Si una extensión carece de la información necesaria, no debe hacer que se produzca un error en la ejecución de la prueba, sino que simplemente debe optar por no participar.

Información genérica
public record KeyValuePairStringProperty(
    string Key,
    string Value)
        : IProperty;

representa los datos de un par clave-valor general.

public record struct LinePosition(
    int Line,
    int Column);

public record struct LinePositionSpan(
    LinePosition Start,
    LinePosition End);

public abstract record FileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : IProperty;

public sealed record TestFileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : FileLocationProperty(FilePath, LineSpan);

se usa para identificar la ubicación de la prueba dentro del archivo de origen. Esto resulta especialmente útil cuando el iniciador es un IDE como Visual Studio o Visual Studio Code.

public sealed record TestMethodIdentifierProperty(
    string AssemblyFullName,
    string Namespace,
    string TypeName,
    string MethodName,
    string[] ParameterTypeFullNames,
    string ReturnTypeFullName)

es un identificador único para un método de prueba.

public sealed record TestMetadataProperty(
    string Key,
    string Value)

se utiliza para transmitir las características o rasgos de .

Información de detección
public sealed record DiscoveredTestNodeStateProperty(
    string? Explanation = null)
{
    public static DiscoveredTestNodeStateProperty CachedInstance { get; }
}

indica que se ha detectado este TestNode. Se utiliza cuando se envía al marco de pruebas. Toma nota del práctico valor en caché que ofrece la propiedad . Esta propiedad es obligatoria.

Información de ejecución
public sealed record InProgressTestNodeStateProperty(
    string? Explanation = null)
{
    public static InProgressTestNodeStateProperty CachedInstance { get; }
}

informa a la plataforma de pruebas que se ha programado para su ejecución y que está actualmente en curso. Toma nota del práctico valor en caché que ofrece la propiedad .

public readonly record struct TimingInfo(
    DateTimeOffset StartTime,
    DateTimeOffset EndTime,
    TimeSpan Duration);

public sealed record StepTimingInfo(
    string Id,
    string Description,
    TimingInfo Timing);

public sealed record TimingProperty : IProperty
{
    public TimingProperty(TimingInfo globalTiming)
        : this(globalTiming, [])
    {
    }

    public TimingProperty(
        TimingInfo globalTiming,
        StepTimingInfo[] stepTimings)
    {
        GlobalTiming = globalTiming;
        StepTimings = stepTimings;
    }

    public TimingInfo GlobalTiming { get; }

    public StepTimingInfo[] StepTimings { get; }
}

se usa para retransmitir detalles de tiempo sobre la ejecución de . También permite el cronometraje de los pasos de ejecución individuales a través de . Esto resulta especialmente útil cuando el concepto de prueba se divide en varias fases, como la inicialización, la ejecución y la limpieza.

Una y solo una de las siguientes propiedades es obligatoria por y comunica el resultado de a la plataforma de prueba.

public sealed record PassedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static PassedTestNodeStateProperty CachedInstance
        { get; } = new PassedTestNodeStateProperty();
}

informa a la plataforma de pruebas que ha sido aprobado. Toma nota del práctico valor en caché que ofrece la propiedad .

public sealed record SkippedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static SkippedTestNodeStateProperty CachedInstance
        { get; } =  new SkippedTestNodeStateProperty();
}

informa a la plataforma de pruebas que este se omitió. Toma nota del práctico valor en caché que ofrece la propiedad .

public sealed record FailedTestNodeStateProperty : TestNodeStateProperty
{
    public FailedTestNodeStateProperty()
        : base(default(string))
    {
    }

    public FailedTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public FailedTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

informa a la plataforma de pruebas que este ha fallado después de una aserción.

public sealed record ErrorTestNodeStateProperty : TestNodeStateProperty
{
    public ErrorTestNodeStateProperty()
        : base(default(string))
    {
    }

    public ErrorTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public ErrorTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

informa a la plataforma de pruebas que este ha fallado. Este tipo de error es diferente de , que se usa para los errores de aserción. Por ejemplo, puede notificar problemas como errores de inicialización de prueba con .

public sealed record TimeoutTestNodeStateProperty : TestNodeStateProperty
{
    public TimeoutTestNodeStateProperty()
        : base(default(string))
    {
    }

    public TimeoutTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public TimeoutTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }

    public TimeSpan? Timeout { get; init; }
}

informa a la plataforma de pruebas que se ha producido un error en este por un motivo de tiempo de espera. Puede notificar el tiempo de espera mediante la propiedad .

public sealed record CancelledTestNodeStateProperty : TestNodeStateProperty
{
    public CancelledTestNodeStateProperty()
        : base(default(string))
    {
    }

    public CancelledTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public CancelledTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

informa a la plataforma de pruebas que este ha fallado debido a la cancelación.