вторник, 31 марта 2015 г.

TestCase в NUnit и Gallio

В NUnitTestCase - это атрибут, однако в Gallio это не так. Т.о. в каждой из обозначенных платформ своя реализация поведения обозначенного элемента. В этой заметке показан пример исходного кода, который успешно компилируется и работает как в случае использования Gallio, так и в случае использования NUnit.

Хорошую книгу о том, как грамотно создавать автономные и интеграционные тесты, в т.ч. и под такие закрытые системы как AutoCAD, я указывал здесь, в п.21. Кроме того, Gallio имеет неплохую offline документацию, доступную в меню Пуск -> Все программы -> Gallio -> Offline Documentation, а документация по NUnit присутствует online.

Gallio может выдавать отчёты о результатах теста как в формате XML, так и в формате HTML. В то же время NUnit может генерировать только XML. Для получения HTML на основе XML отчётов NUnit я рекомендую пользоваться утилитой NUnitOrange.

Полагаю, что лучше всего различие продемонстрирует исходный код:

/* © Andrey Bushman, 2015
 * Tests.cs
 * Recommended format of naming of tests: 
 * UnitOfWorkName_Scenario_ExpectedBehavior
 */
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
 
#if NUNIT
using Fw = NUnit.Framework;
 
#elif GALLIO
using Gallio.Framework;
using Gallio.Model;
using Fw = MbUnit.Framework;
#endif
 
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
 
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
 
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
 
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
 
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
 
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
 
using Ex = Bushman.CAD.Extensions.ExtensionSample.UnitTests;
 
namespace Bushman.CAD.Extensions.ExtensionSample.UnitTests {
  [Fw.TestFixture,
#if NUNIT
 Fw.Apartment(ApartmentState.STA)
#endif
]
  public class Tests {
 
    const String blockName = "TEMP_BLOCK";
 
    // ***********************************************************************
 
    [Fw.Ignore("Sample of ignored test.")]
    [Fw.Test]
    [Fw.Category("Autodesk API")]
    [Fw.Description("Some description")]
    public void HasAttributeDefinitions_WhenAttribsExist_IsTrue() {
      // Create new temp database
      using (Db.Database db = new Db.Database(truetrue)) {
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            Db.ObjectId id = CreateBlockDefinition(db);
            Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
              Db.BlockTableRecord;
 
            Fw.Assert.IsTrue(btr.HasAttributeDefinitions);
            tr.Commit();
          }
        }
      } // close and discard changes
    }
 
    [Fw.Test]
    [Fw.Category("Autodesk API")]
    [Fw.Description("This test shows AutoCAD .NET API bug. It exists in " +
      "AutoCAD 2009-2016.")]
    public void HasAttributeDefinitions_WhenAttribsInNotExist_IsFalse() {
      Db.ObjectId id = Db.ObjectId.Null;
      // Create new temp database
      using (Db.Database db = new Db.Database(truetrue)) {
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            // create new block definition with an attribute definition
            id = CreateBlockDefinition(db);
            Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
              Db.BlockTableRecord;
            // remove all attribute definitions
            String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
              .Name;
            foreach (Db.ObjectId itemId in btr) {
              if (itemId.ObjectClass.Name == name) {
                Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
                obj.Erase(true);
              }
            }
            tr.Commit();
          }
        }
        // Check attribute definition count
        using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
          Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
            Db.BlockTableRecord;
 
          Fw.Assert.IsFalse(btr.HasAttributeDefinitions);
          tr.Commit();
        }
      } // close and discard changes
    }
 
    // ***********************************************************************
 
    [Fw.Test]
    [Fw.Category("Bushman API")]
    [Fw.Description("I am some description 1 :)")]
    public void HasAttDefs_WhenAttribsExist_IsTrue() {
      // Create new temp database
      using (Db.Database db = new Db.Database(truetrue)) {
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            Db.ObjectId id = CreateBlockDefinition(db);
            Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
              Db.BlockTableRecord;
 
            Fw.Assert.IsTrue(btr.HasAttDefs());
            tr.Commit();
          }
        }
      } // close and discard changes
    }
 
    [Fw.Test]
    [Fw.Category("Bushman API")]
    [Fw.Description("I am some description 2 :)")]
    public void HasAttDefs_WhenAttribsIsNotExist_IsFalse() {
      Db.ObjectId id = Db.ObjectId.Null;
      // Create new temp database
      using (Db.Database db = new Db.Database(truetrue)) {
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            id = CreateBlockDefinition(db);
            Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
              Db.BlockTableRecord;
            // remove all attribute definitions
            String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
              .Name;
            foreach (Db.ObjectId itemId in btr) {
              if (itemId.ObjectClass.Name == name) {
                Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
                obj.Erase(true);
              }
            }
            tr.Commit();
          }
 
          // Check attribute definition count
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
              Db.BlockTableRecord;
 
            Fw.Assert.IsFalse(btr.HasAttDefs());
            tr.Commit();
          }
        }
      } // close and discard changes
    }
 
    // **********************************************************************
 
    // Creating of the temp block with an instance of AttributeDefinition
    internal static Db.ObjectId CreateBlockDefinition(Db.Database db) {
      if (null == db || db.IsDisposed) {
        throw new ArgumentException("null == db || db.IsDisposed");
      }
 
      Db.ObjectId id = Db.ObjectId.Null;
 
      // Create a temp block definition with an AttributeDefinition instance
      using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
        Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForWrite
          ) as Db.BlockTable;
        Db.BlockTableRecord btr = new Db.BlockTableRecord();
        btr.Name = blockName;
 
        // its content: the circle and attribite definition
        Db.Circle circle = new Db.Circle();
        circle.SetDatabaseDefaults();
        circle.Radius = 20.0;
        circle.Center = Gm.Point3d.Origin;
        circle.ColorIndex = 50;
 
        btr.AppendEntity(circle);
 
        Db.AttributeDefinition atDef = new Db.AttributeDefinition(
          circle.Center, "Hello!""ATTRIB""New value",
          Us.GetTextStyleStandardId(db));
        atDef.SetDatabaseDefaults();
 
        btr.AppendEntity(atDef);
 
        id = bt.Add(btr);
        tr.AddNewlyCreatedDBObject(btr, true);
 
        tr.Commit();
      }
      return id;
    }
    // **********************************************************************
 
    // This test template reads the Database from the DWG file for own work.
    [Fw.Test]
    [Fw.Category("TestCase using samples")]
    public void CircleIsExists() {
      const String dwgFileName = @"..\NUnit\data-for-testing\data_01.dwg";
      Boolean result = false;
      // Read Database from the DWG file
      using (Db.Database db = new Db.Database(truetrue)) {
        db.ReadDwgFile(dwgFileName, Db.FileOpenMode.OpenForReadAndWriteNoShare,
          false"");
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction())
          {
            Db.ObjectId msId = Us.GetBlockModelSpaceId(db);
            Db.BlockTableRecord ms = tr.GetObject(msId, Db.OpenMode.ForRead) 
              as Db.BlockTableRecord;
            Rt.RXClass rxc = Rt.RXClass.GetClass(typeof (Db.Circle));
            Db.ObjectId id = ms.Cast<Db.ObjectId>().FirstOrDefault();
            result = Db.ObjectId.Null != id;
 
            tr.Commit();
          }
          Fw.Assert.IsTrue(result);
        }
      } // close and discard changes
    }
 
    // **********************************************************************
 
#if GALLIO
    [Fw.StaticTestFactory]
    public static IEnumerable<Fw.Test> TestSuite_RenameMe() {
      yield return new Fw.TestSuite("Gallio tests some suite") {
        Description = "An example test suite.",
        Metadata =
        {
            { MetadataKeys.Category, "TestCase using samples" },
            { MetadataKeys.Description, "I am some description 3 :)" }
        },
        Timeout = TimeSpan.FromMinutes(2),
        Children =
        {
            new Fw.TestCase("CircleMustToBeExisting", () => {
              __CheckingOfCircleExisting(@"..\NUnit\data-for-testing\data_01.dwg", true);
            }),
            new Fw.TestCase("CircleMustNotToBeExisting", () => {
              __CheckingOfCircleExisting(@"..\NUnit\data-for-testing\data_02.dwg", false);
            })
        }
      };
    }
#elif NUNIT
    [Fw.Test]
    [Fw.Category("TestCase using samples")]
    [Fw.TestCase(@"..\NUnit\data-for-testing\data_01.dwg"true)]
    [Fw.TestCase(@"..\NUnit\data-for-testing\data_02.dwg"false)]
#endif
    public void CheckingOfCircleExisting(String dwgFileName, 
      Boolean expectedResult) {
      __CheckingOfCircleExisting(dwgFileName, expectedResult);
    }
 
    static void __CheckingOfCircleExisting(String dwgFileName, 
      Boolean expectedResult) {
      // Read Database from the DWG file
      using (Db.Database db = new Db.Database(truetrue)) {
        db.ReadDwgFile(dwgFileName, Db.FileOpenMode.OpenForReadAndWriteNoShare,
          false"");
        Boolean result = false;
        using (new WorkingDatabaseSwitcher(db)) {
          using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
            Db.ObjectId msId = Us.GetBlockModelSpaceId(db);
            Db.BlockTableRecord ms = tr.GetObject(msId, Db.OpenMode.ForRead)
              as Db.BlockTableRecord;
            Rt.RXClass rxc = Rt.RXClass.GetClass(typeof(Db.Circle));
            
            result = ms.Cast<Db.ObjectId>().Any(n=>n.ObjectClass.IsDerivedFrom(rxc));
            tr.Commit();
          }
          Fw.Assert.AreEqual(expectedResult, result);
        }
      } // close and discard changes
    }
 
    // **********************************************************************
  }
}
#endif

Результаты тестов, в пакетном режиме автоматически произведённых в AutoCAD 2009-2015 и представленных в формате HTML можно скачать и посмотреть отсюда. Пакетное тестирование происходило с использованием acad.exe для AutoCAD 2009-2012 и accoreconsole.exe для AutoCAD 2013-2015. Тесты для AutoCAD 2009 и 2010 скомпилированы с использованием платформы Gallio. Тесты для AutoCAD 2011-2015 собраны с использованием NUnit. Внешнее представление отчётов в HTML-формате у этих платформ несколько отличается, но оба варианта достаточно удобны для использования.

В коде тестов я активно использую один из своих вспомогательных классов: WorkingDatabaseSwitcher. Вот его исходный код:

/* © Andrey Bushman, 2015
 * WorkingDatabaseSwitcher.cs
 */
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
 
#if NUNIT
using Fw = NUnit.Framework;
 
#elif GALLIO
using Gallio.Framework;
using Fw = MbUnit.Framework;
#endif
 
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
 
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
 
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
 
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
 
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
 
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
 
using Ex = Bushman.CAD.Extensions.ExtensionSample.UnitTests;
 
namespace Bushman.CAD.Extensions.ExtensionSample.UnitTests {
 
  /// <summary>
  /// This class switches the WorkingDatabase. It was created for using in the 
  /// tests.
  /// </summary>
  internal sealed class WorkingDatabaseSwitcher : IDisposable {
 
    Db.Database oldDb = null;
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="db">Target database.</param>
    public WorkingDatabaseSwitcher(Db.Database db) {
      oldDb = Hs.WorkingDatabase;
      Hs.WorkingDatabase = db;
    }
 
    public void Dispose() {
      Hs.WorkingDatabase = oldDb;
    }
  }
}
#endif

В обозначенных выше тестах я тестирую в том числе и некоторый метод HasAttDefs(), вот его исходный код:

/* © Andrey Bushman, 2015
 * ExtensionMethods.cs
 */
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
 
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
 
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
 
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
 
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
 
#if AUTOCAD_NEWER_THAN_2009
using In = Autodesk.AutoCAD.Internal;
#endif
 
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
 
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
 
using Ex = Bushman.CAD.Extensions.ExtensionSample;
 
namespace Bushman.CAD.Extensions.ExtensionSample {
  public static class ExtensionMethods {
    /// <summary>
    /// This method is a replace for the 
    /// <c>BlockTableRecord.HasAttributeDefinitions</c> method. Implementation 
    /// by Autodesk works wrong: it returns <c>True</c> after the 
    /// <c>AttributeDefinition</c> instance was deleted from the 
    /// <c>BlockTableRecord</c>. Info source: 
    /// http://bushman-andrey.blogspot.ru/2014/03/blocktablerecordhasattributedefinitions.html
    /// </summary>
    /// <param name="btr">Target instance of the <c>BlockTableRecord</c> class.
    /// </param>
    /// <returns>returns true or false.</returns>
    public static Boolean HasAttDefs(this Db.BlockTableRecord btr) {
      String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition)).Name;
      return btr.Cast<Db.ObjectId>().Any(n => !n.IsNull && n.IsValid
          && !n.IsErased && !n.IsEffectivelyErased && String.Equals(
          n.ObjectClass.Name, name, StringComparison.InvariantCulture));
    }
  }
}
#endif

Комментариев нет: