2009-03-05 74 views
15

He leído acerca de las pruebas unitarias y he escuchado mucho alboroto por parte de otros que pregonan su utilidad, y me gustaría verlo en acción. Como tal, he seleccionado esta clase básica de una aplicación simple que he creado. No tengo idea de cómo me podrían ayudar las pruebas, y espero que alguno de ustedes me ayude a ver su beneficio al señalar qué partes de este código se pueden probar y cómo se verán esas pruebas. Entonces, ¿cómo escribiría pruebas unitarias para el siguiente código?Ejemplos de práctica Prueba del código C#

public class Hole : INotifyPropertyChanged 
{ 
    #region Field Definitions 
    private double _AbsX; 
    private double _AbsY; 
    private double _CanvasX { get; set; } 
    private double _CanvasY { get; set; } 
    private bool _Visible; 
    private double _HoleDia = 20; 
    private HoleTypes _HoleType; 
    private int _HoleNumber; 
    private double _StrokeThickness = 1; 
    private Brush _StrokeColor = new SolidColorBrush(Colors.Black); 
    private HolePattern _ParentPattern; 
    #endregion 

    public enum HoleTypes { Drilled, Tapped, CounterBored, CounterSunk }; 
    public Ellipse HoleEntity = new Ellipse(); 
    public Ellipse HoleDecorator = new Ellipse(); 
    public TextBlock HoleLabel = new TextBlock(); 

    private static DoubleCollection HiddenLinePattern = 
       new DoubleCollection(new double[] { 5, 5 }); 

    public int HoleNumber 
    { 
     get 
     { 
      return _HoleNumber; 
     } 
     set 
     { 
      _HoleNumber = value; 
      HoleLabel.Text = value.ToString(); 
      NotifyPropertyChanged("HoleNumber"); 
     } 
    } 
    public double HoleLabelX { get; set; } 
    public double HoleLabelY { get; set; } 
    public string AbsXDisplay { get; set; } 
    public string AbsYDisplay { get; set; } 

    public event PropertyChangedEventHandler PropertyChanged; 
    //public event MouseEventHandler MouseActivity; 

    // Constructor 
    public Hole() 
    { 
     //_HoleDia = 20.0; 
     _Visible = true; 
     //this.ParentPattern = WhoIsTheParent; 
     HoleEntity.Tag = this; 
     HoleEntity.Width = _HoleDia; 
     HoleEntity.Height = _HoleDia; 

     HoleDecorator.Tag = this; 
     HoleDecorator.Width = 0; 
     HoleDecorator.Height = 0; 


     //HoleLabel.Text = x.ToString(); 
     HoleLabel.TextAlignment = TextAlignment.Center; 
     HoleLabel.Foreground = new SolidColorBrush(Colors.White); 
     HoleLabel.FontSize = 12; 

     this.StrokeThickness = _StrokeThickness; 
     this.StrokeColor = _StrokeColor; 
     //HoleEntity.Stroke = Brushes.Black; 
     //HoleDecorator.Stroke = HoleEntity.Stroke; 
     //HoleDecorator.StrokeThickness = HoleEntity.StrokeThickness; 
     //HiddenLinePattern=DoubleCollection(new double[]{5, 5}); 
    } 

    public void NotifyPropertyChanged(String info) 
    { 
     if (PropertyChanged != null) 
     { 
      PropertyChanged(this, 
         new PropertyChangedEventArgs(info)); 
     } 
    } 

    #region Properties 
    public HolePattern ParentPattern 
    { 
     get 
     { 
      return _ParentPattern; 
     } 
     set 
     { 
      _ParentPattern = value; 
     } 
    } 

    public bool Visible 
    { 
     get { return _Visible; } 
     set 
     { 
      _Visible = value; 
      HoleEntity.Visibility = value ? 
      Visibility.Visible : 
      Visibility.Collapsed; 
      HoleDecorator.Visibility = HoleEntity.Visibility; 
      SetCoordDisplayValues(); 
      NotifyPropertyChanged("Visible"); 
     } 
    } 

    public double AbsX 
    { 
     get { return _AbsX; } 
     set 
     { 
      _AbsX = value; 
      SetCoordDisplayValues(); 
      NotifyPropertyChanged("AbsX"); 
     } 
    } 

    public double AbsY 
    { 
     get { return _AbsY; } 
     set 
     { 
      _AbsY = value; 
      SetCoordDisplayValues(); 
      NotifyPropertyChanged("AbsY"); 
     } 
    } 

    private void SetCoordDisplayValues() 
    { 
     AbsXDisplay = HoleEntity.Visibility == 
     Visibility.Visible ? String.Format("{0:f4}", _AbsX) : ""; 
     AbsYDisplay = HoleEntity.Visibility == 
     Visibility.Visible ? String.Format("{0:f4}", _AbsY) : ""; 
     NotifyPropertyChanged("AbsXDisplay"); 
     NotifyPropertyChanged("AbsYDisplay"); 
    } 

    public double CanvasX 
    { 
     get { return _CanvasX; } 
     set 
     { 
      if (value == _CanvasX) { return; } 
      _CanvasX = value; 
      UpdateEntities(); 
      NotifyPropertyChanged("CanvasX"); 
     } 
    } 

    public double CanvasY 
    { 
     get { return _CanvasY; } 
     set 
     { 
      if (value == _CanvasY) { return; } 
      _CanvasY = value; 
      UpdateEntities(); 
      NotifyPropertyChanged("CanvasY"); 
     } 
    } 

    public HoleTypes HoleType 
    { 
     get { return _HoleType; } 
     set 
     { 
      if (value != _HoleType) 
      { 
       _HoleType = value; 
       UpdateHoleType(); 
       NotifyPropertyChanged("HoleType"); 
      } 
     } 
    } 

    public double HoleDia 
    { 
     get { return _HoleDia; } 
     set 
     { 
      if (value != _HoleDia) 
      { 
       _HoleDia = value; 
       HoleEntity.Width = value; 
       HoleEntity.Height = value; 
       UpdateHoleType(); 
       NotifyPropertyChanged("HoleDia"); 
      } 
     } 
    } 

    public double StrokeThickness 
    { 
     get { return _StrokeThickness; } 
     //Setting this StrokeThickness will also set Decorator 
     set 
     { 
      _StrokeThickness = value; 
      this.HoleEntity.StrokeThickness = value; 
      this.HoleDecorator.StrokeThickness = value; 
      NotifyPropertyChanged("StrokeThickness"); 
     } 
    } 

    public Brush StrokeColor 
    { 
     get { return _StrokeColor; } 
     //Setting this StrokeThickness will also set Decorator 
     set 
     { 
      _StrokeColor = value; 
      this.HoleEntity.Stroke = value; 
      this.HoleDecorator.Stroke = value; 
      NotifyPropertyChanged("StrokeColor"); 
     } 
    } 

    #endregion 

    #region Methods 

    private void UpdateEntities() 
    { 
     //-- Update Margins for graph positioning 
     HoleEntity.Margin = new Thickness 
     (CanvasX - HoleDia/2, CanvasY - HoleDia/2, 0, 0); 
     HoleDecorator.Margin = new Thickness 
     (CanvasX - HoleDecorator.Width/2, 
     CanvasY - HoleDecorator.Width/2, 0, 0); 
     HoleLabel.Margin = new Thickness 
     ((CanvasX * 1.0) - HoleLabel.FontSize * .3, 
     (CanvasY * 1.0) - HoleLabel.FontSize * .6, 0, 0); 
    } 

    private void UpdateHoleType() 
    { 
     switch (this.HoleType) 
     { 
      case HoleTypes.Drilled: //Drilled only 
       HoleDecorator.Visibility = Visibility.Collapsed; 
       break; 
      case HoleTypes.Tapped: // Drilled & Tapped 
       HoleDecorator.Visibility = (this.Visible == true) ? 
       Visibility.Visible : Visibility.Collapsed; 
       HoleDecorator.Width = HoleEntity.Width * 1.2; 
       HoleDecorator.Height = HoleDecorator.Width; 
       HoleDecorator.StrokeDashArray = 
       LinePatterns.HiddenLinePattern(1); 
       break; 
      case HoleTypes.CounterBored: // Drilled & CounterBored 
       HoleDecorator.Visibility = (this.Visible == true) ? 
       Visibility.Visible : Visibility.Collapsed; 
       HoleDecorator.Width = HoleEntity.Width * 1.5; 
       HoleDecorator.Height = HoleDecorator.Width; 
       HoleDecorator.StrokeDashArray = null; 
       break; 
      case HoleTypes.CounterSunk: // Drilled & CounterSunk 
       HoleDecorator.Visibility = (this.Visible == true) ? 
       Visibility.Visible : Visibility.Collapsed; 
       HoleDecorator.Width = HoleEntity.Width * 1.8; 
       HoleDecorator.Height = HoleDecorator.Width; 
       HoleDecorator.StrokeDashArray = null; 
       break; 
     } 
     UpdateEntities(); 
    } 

    #endregion 

} 

Respuesta

1

Aquí hay un ejemplo. Tenga en cuenta el código de ejemplo carecía de definiciones de una serie de dependencias:

[TestFixture()] 
public class TestHole 
{ 

    private Hole _unitUnderTest; 

    [SetUp()] 
    public void SetUp() 
    { 
     _unitUnderTest = new Hole(); 
    } 

    [TearDown()] 
    public void TearDown() 
    { 
     _unitUnderTest = null; 
    } 

    [Test] 
    public void TestConstructorHole() 
    { 
     Hole testHole = new Hole(); 
     Assert.IsNotNull(testHole, "Constructor of type, Hole failed to create instance."); 
    } 

    [Test] 
    public void TestNotifyPropertyChanged() 
    { 
     string info = null; 
     _unitUnderTest.NotifyPropertyChanged(info); 
    } 
} 

Se puede ver que se está probando que el constructor está produciendo un objeto válido (por lo general no es necesario con un accesorio de prueba completa en su lugar, la construcción es generalmente bien ejercitado) y también está probando el único método público en la clase. En este caso necesitaría un delegado controlador de eventos y un Assert para verificar el contenido del parámetro de información.

El objetivo es escribir pruebas que ejerciten cada método de su clase. Por lo general, esto incluye límites superiores e inferiores, así como las condiciones de falla.

4

No se puede probar correctamente este código a menos que también se especifique. "Pruebas" generalmente significa asegurarse de que el software funcione según lo diseñado.

EDITAR: Esto realmente no es una respuesta "cop out". He trabajado antes como probador y puedo decirles que casi todos los casos de prueba que escribí se derivaron directamente de las especificaciones del software.

+0

Beats en un lugar donde trabajé, donde las pruebas fueron creadas por el desarrollador de acuerdo con la idea del desarrollador de la especificación. Siempre sentí que estaba haciendo trampa al hacer mis propias pruebas. –

+0

Hacer sus propias pruebas no es hacer trampa. Dicen que entendiste la especificación y pudiste dividirla en componentes que en su totalidad son lo que el consumidor del software quiere. – flq

0

un ejemplo,

para los

HoleTypes públicas de orificio

prueba/verificación para nulo en el conjunto

6

Unidad de Prueba Ejemplo:

  • verificar que PropertyChanged Evento se dispara con los eventos args correctos. Use la reflexión en la prueba para repetir todas las propiedades que establecen los valores y verificar el evento.

Normalmente esto se hace con un marco de prueba como NUnit.

(tipo de causa divertido se dará cuenta de que la propiedad ParentPattern no se dispara el evento.)

+0

Las pruebas que usan reflexión no son tan buenas. Para mí, sugieren algunos problemas de diseño. Además de esto, es muy probable que estas pruebas se rompan y no estén claras. –

+0

No soy un probador, pero aún no puedo encontrar una buena manera de automatizar a través de pruebas unitarias si un evento se dispara o no. –

+0

Reflection le permite olvidarse de esta prueba una vez que la codifica. En este ejemplo, probamos que un evento se activa cuando una propiedad cambia en esta entidad. ¿Pensaría un nuevo desarrollador volver a la prueba cuando se agrega una nueva propiedad? ¿La prueba sigue siendo verde si no prueba todas las propiedades? – Gord

2

Las pruebas no es sólo la ingeniería - es un arte. Algo que requiere que leas. No estoy tan seguro de que podamos enseñarle a través de esta única pregunta todo lo que desea/necesita/debería/debería/debe saber. Para comenzar, aquí hay algunas cosas que puedes probar.

  • Unidad (interfaces de trabajo como se esperaba)
  • Integración (componentes se comportan entre sí)
  • Usabilidad (clientes están satisfechos)
  • funcional (función completa)

Definir un conjunto de criterios contra cada uno (métricas) y comenzar a probar.

+0

No es un arte. Puede ser artístico, pero no es un arte. – OscarRyz

+0

Veo a la ingeniería como una forma de arte sin grasa. – dirkgently

+0

Bueno, esa puede ser la razón. En mi caso, creo que el arte no tiene otro propósito que la "re-creación" del ser humano. Si el propósito de su código (o cualquier otra cosa) main es distinto de eso, entonces no es arte (para mí). Por ejemplo, el primer objetivo de prueba es validar el software. :) :) Paz. – OscarRyz

1

Una especie de un lado, parece que la mayoría de esta clase no debería necesitar ser probado (que no sea Gord's answer), si la clase fue escrito de una manera diferente. Por ejemplo, está entremezclando información del modelo (tipo de agujero, etc.) con información de vista (grosor). Además, creo que te estás perdiendo el sentido de WPF y de los enlaces de datos/disparadores. UpdateHoleType() Debe expresarse en el archivo .xaml como un conjunto de DataTriggers, y lo mismo con UpdateEntities(), y la mayoría de las otras propiedades que tiene.

+0

+1 para señalar la combinación de información de "modelo" y "vista". (Pensé en esto también, pero no en una moda MVC.) – strager

2

En la prueba unitaria, solo prueba sus métodos/propiedades "visibles" y no los privados.

Así por ejemplo, en el código que puede agregar la siguiente prueba: "bueno eso es obvio "

hole.Visible = false; 

Debug.Assert("".Equals(hole.AbsXDisplay)); 
Debug.Assert("".Equals(hole.AbsYDisplay)); 

Se podría pensar pero después de algunas semanas, puede olvidarse de eso. Y si alguna parte de su código depende del valor de AbsXDisplay (que es un atributo público) y por alguna razón después de establecer la propiedad en falso, ya no es "" sino "vacío" o "NotSet", entonces esto la prueba fallará y se te notificará de inmediato.

Se supone que debe hacer esto para cada método público o atributo que tenga alguna regla de negocios o que afecte a alguna otra parte de su clase.

Algunas personas encuentran más fácil de probar primero (y hacer que la prueba falle) y luego el código para satisfacer el ensayo, de esa manera código sólo lo que se prueba, y se prueba sólo lo que importa (búsqueda de TDD)

Eso fue solo un simple ejemplo de lo que puede hacer para probar su código.

Espero que te ayude a darte una buena idea de lo que se trata la prueba.

Como otros han sugerido, busque un marco de prueba.

+0

Hay valor en probar métodos privados. "Glassbox" frente a "blackbox". A menudo puedes escribir una prueba más estrecha, más nítida, más simple y más reveladora probando lo privado en lugar del público. Las pruebas de Glassbox se deben cambiar cuando cambian los métodos privados, pero eso es parte de la refactorización normal. – Schwern

-1

Bueno, la historia comienza con la teoría.

Esto es lo que he hecho.

Primero, si programa en lenguaje OO, aprenda patrones de diseño. Es más fácil si forma un grupo de estudio y aprende junto con algunos amigos y colegas.

Pase varios meses para digerir fácilmente todos los patrones.

Luego, pase a técnicas de refactorización donde aprenderá a transformar cualquier código en código que utilizará bloques aprendidos previamente, p. patrones de diseño.

Después de esta preparación, las pruebas serán tan fáciles como cualquier otra técnica de programación.

+0

¡Todas las refactorizaciones, excepto las más simples, REQUIEREN pruebas! Y solo si aprende patrones como ejercicios mecánicos, se restringirían a OO. Ese orden es horrible. Las pruebas, la refactorización y los patrones se deben aprender en paralelo con énfasis en las pruebas. OO está en un conjunto completamente diferente. – Schwern

+0

@Schwern Tienes toda la razón. Estaba hablando estrictamente acerca de la forma en que había aprendido a probar. El problema es con el arranque. Cómo comenzar? ¿Qué aprender al principio? Tienes que empezar de algo, ¿no? –

4

Te contaré el gran misterio de las pruebas.

Cuando escribe una prueba, está escribiendo un software que verifica otro software. Comprueba que tus suposiciones son verdaderas. Tus suposiciones son simplemente declaraciones. Aquí hay una prueba simple y tonta que además funciona.

if(1 + 1 == 2) { 
    print "ok - 1 plus 1 equals 2\n"; 
} 
else { 
    print "not ok\n"; 
} 

Esas declaraciones, esas afirmaciones, deben ser ciertas o si no, hay un error o una característica que falta. Esto detecta errores más rápidamente, antes de que se conviertan en errores peludos y sistemáticos, antes de que el usuario los vea. La falla apunta a un problema que debe ser resuelto. Idealmente, también le brinda suficiente información para diagnosticar el problema. La prueba enfocada y los diagnósticos hacen que la depuración sea mucho más rápida.

Usted está escribiendo este software para que haga su trabajo por usted. Para hacerlo mejor de lo que puedes. Puede probar el software a mano, echando un vistazo a la salida, pero las pruebas una vez escritas no desaparecen. Construyen y construyen y desarrollan hasta que hay una gran masa de ellos probando nuevas características, características antiguas, nuevos errores y viejos errores. La tarea de probar tu nuevo código a mano, así como asegurarte de que no has reintroducido algún viejo error, rápidamente se vuelve abrumador. Un humano simplemente dejará de probar los viejos errores. Serán reintroducidos y el tiempo será desperdiciado. Un programa de prueba puede hacer todo esto por usted con solo presionar un botón. Es una tarea aburrida y rutinaria. Los humanos los chupan, es por eso que inventamos las computadoras. Al escribir un software para probar su software, está usando la computadora para lo que se pretendía: ahorrar tiempo.

Lo expreso en términos tan simplistas porque las personas que son nuevas en las pruebas a menudo se sienten abrumadas. Ellos piensan que hay algo de magia. Algunos marcos especiales que tienen que usar. A menudo incluso se olvidan de que las pruebas siguen siendo programas y de repente no pueden pensar en usar un ciclo o escribir una subrutina. Hay mucho, mucho más que aprender, pero espero que esto le proporcione un núcleo alrededor del cual averiguar qué es esta cosa de "prueba".

+1

Un poco demasiado difícil, pero sigue siendo una buena respuesta. – strager

0

Debemos probarlo, ¿verdad?

Las pruebas son la validación de que el código funciona como espera que funcione. Escribir pruebas para esta clase en este momento no le reportará ningún beneficio real (a menos que descubra un error al escribir las pruebas). El beneficio real es cuando tendrá que regresar y modificar esta clase. Puede estar utilizando esta clase en varios lugares diferentes en su aplicación. Sin pruebas, los cambios en la clase pueden tener repercusiones imprevistas. Con las pruebas, puede cambiar la clase y estar seguro de que no está rompiendo otra cosa si todas sus pruebas pasan. Por supuesto, las pruebas deben estar bien escritas y cubrir todas las funcionalidades de la clase.

Entonces, ¿cómo probarlo?

En el nivel de clase, tendrá que escribir pruebas de unidad. Hay varios marcos de prueba de unidades. Prefiero NUnit.

¿Qué estoy haciendo?

Está probando que todo se comporta como espera que se comporte. Si le da un método X, entonces espera que se devuelva Y. En Gord's respuesta, sugirió probar que su evento realmente se dispara. Esta sería una buena prueba.

El libro, Agile Principles, Patterns, and Practices in C# de Uncle Bob realmente me ha ayudado a entender qué y cómo probarlo.

1

Al probar, supongo que se refiere al diseño impulsado por pruebas. El diseño basado en pruebas se ocupa principalmente de pruebas unitarias y, a veces, pruebas de integración. Las pruebas unitarias prueban los elementos de código comprobables más pequeños y las pruebas de integraciones prueban la interacción entre componentes con su aplicación.

Hay muchas más formas de prueba, pero estas son las que suelen hacer los desarrolladores. Las otras pruebas principalmente miran la aplicación desde fuera y prueban las interfaces de usuario expuestas para varias cualidades como la corrección, el rendimiento y la escalabilidad.

Las pruebas unitarias implican probar sus métodos para ver si hacen lo que usted quiere. Por lo general, estas pruebas son tan simples que casi pensarías que son triviales. Lo que buscas probar es la lógica de tu clase. La clase que proporcionas realmente no tiene tanta lógica.

Solo el private void UpdateHoleType(){...} contiene cualquier lógica que parezca lógica visualmente orientada, siempre la más difícil de probar. Escribir una prueba es muy simple. A continuación se muestra un ejemplo del tipo de agujero perforado.

[Test] 
public void testDrilledHole() 
{ 
    Hole hole = new Hole(); 
    hole.HoleType = HoleTypes.Drilled; 
    Assert.AreEqual(Visibility.Collapsed, hole.HoleDecorator.Visibility); 
} 

Si lo miras, casi no lo consideras que vale la pena. La prueba es trivial y obvia. El atributo [Test] declara el método como una prueba y el método Assert.AreEquals() arroja una excepción si los valores proporcionados no son iguales. La construcción real puede variar dependiendo del marco de prueba utilizado, pero todos son igualmente simples.

El truco aquí es que escribe estos métodos para todos los métodos en su clase realizando lógica de negocios y probando una serie de valores. null siempre es un buen valor para probar.

La potencia de las pruebas unitarias está en la combinación de todas esas pruebas. Ahora bien, si cambia algo en la clase, hay una serie de pruebas que comprueban si un cambio que realizó rompe uno de los comportamientos que definió en la prueba. Esto le permite trabajar con un proyecto más grande, cambiando e implementando nuevas características mientras que las pruebas conservan la funcionalidad que ya ha codificado.

1

Las pruebas ayudarán, si necesita realizar cambios.

Según plumas (plumas, Working Effectively with Legacy Code, p. 3) hay cuatro razones para cambios:

  • Agregando una característica
  • la fijación de un bug
  • mejorar el diseño de
  • uso de recursos Optimización

Cuando hay una necesidad de cambio, quiere estar seguro de que no se rompe nada ng. Para ser más preciso: no desea romper ningún comportamiento (Hunt, Thomas, Pragmatic Unit Testing in C# with NUnit, p.31).

Con las pruebas unitarias en su lugar, puede hacer cambios con mucha más confianza, porque (siempre que estén programados correctamente) capturarán los cambios de comportamiento. Ese es el beneficio de las pruebas unitarias.

Sería difícil hacer pruebas unitarias para la clase que proporcionó como ejemplo, porque las pruebas unitarias también requieren una cierta estructura del código bajo prueba. Una razón por la que veo es que la clase está haciendo demasiado. Cualquier prueba unitaria que aplicará en esa clase será bastante frágil. Un cambio menor puede hacer que exploten las pruebas de su unidad y terminará perdiendo mucho tiempo solucionando problemas en su código de prueba en lugar de su código de producción.

Para obtener los beneficios de las pruebas unitarias es necesario cambiar el código de producción.Simplemente aplicando los principios de las pruebas unitarias, sin considerar esto, no obtendremos la experiencia positiva de pruebas unitarias.

¿Cómo obtener la experiencia positiva de la unidad de prueba? Sea de mente abierta y aprenda.

Te recomendaría Working Effectively with Legacy Code para una base de código existente (como la parte del código que proporcionaste más arriba). Para un inicio fácil de la prueba unitaria prueba Pragmatic Unit Testing in C# with NUnit. La verdadera revelación fue para mí xUnit Test Patterns: Refactoring Test Code.

¡Buena suerte en tu viaje!

0

En cuanto al caso que le notifique a disparar sin duda debe asegurarse de si su clase funciona de acuerdo a las especificaciones, a saber, que:

  • los padres nunca se disparará independientemente del valor establecido
  • StrokeColour y StrokeThickness siempre disparar el caso, a pesar de que el mismo valor se establece
  • CanvasX/Y, de orificio/Dia fuego sólo cuando un valor diferente que el anterior se establece

entonces Y Desea comprobar un par de efectos secundarios que provocan la configuración de sus propiedades. Después de eso, podrías pensar en refacturar la cosa porque, dang, ¡esta no es una clase bonita!

Cuestiones relacionadas