Архив рубрики: NUnit

Опубликован проект AcadKeyParser

Выложил на BitBucket проект AcadKeyParser. Информацию о назначении библиотеки, примеры её использования и откомпилированные версии под .NET 3.5, 4.0, 4.5 и 4.6 разместил там же. В составе решения присутствуют модульные тесты (NUnit) и консольная утилита, позволяющая интерактивно проверять "валидность" ключей AutoCAD. Результаты модульных тестов автоматом оформляются в виде HTML отчёта при помощи утилиты ReportUnit (более детальную информацию см. в файле run_me.bat).

NUnit: вывод сообщений трассировщика

По умолчанию, GUI NUnit не отображает сообщения трассировщика. Однако это поведение можно изменить в настройках программы.

Для сборок Release я, обычно, предпочитаю отключать компиляцию кода трассировки.


Но для сборок, собранных в режиме DEBUG, обозначенную директиву компиляции всегда полезно активировать. Итак, для того, чтобы GUI NUnit отображал сообщения трассировщика, встречающиеся в нашем исходном коде, например: что-то вроде этого:

CultureInfo culture = null;
if (!valid_lcids.TryGetValue(lcid, out culture)) {
  Trace.TraceError(String.Format("Incorrect LCID value: {0}.\n", lcid));
  return false;
}

необходимо в настройках NUnit включить соответствующую галочку. Группа интересующих нас настроек (как видим, она позволяет управлять отображением контента и из др. потоков):



Обозначенный ниже текст выведен именно трассировщиком:



NUnit: Тестирование DLL, использующей внешние данные

В составе одной из моих DLL присутствует функционал, позволяющий проверять на предмет корректности выражения вида ACAD-E001:409 (а так же создавать их) и, если выражение корректно, то полностью расшифровывать эту информацию. Для работы обозначенной DLL необходим специальный XML файл, содержащий информацию о существующих версиях AutoCAD и их вертикальных продуктах. Этот файл размещается в том же каталоге, в котором находится DLL. Для тестирования обозначенного функционала я написал пару тестов и запустил их в GUI NUnit...


Код моей DLL находит нужный XML следующим образом:

String asmLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly(
  ).Location);

// xml file with AutoCAD products data.
String xmlFile = Path.Combine(asmLocation, "cad.data.xml");
if (!File.Exists(xmlFile)) {
  throw new FileNotFoundException(xmlFile);
}

Результаты компиляции размещены в каталоге "C:\Users\developer\Documents\Visual Studio 2013\Projects\cad-solution\cad.UnitTests\bin\Debug". Запустив тесты на исполнение я увидел, что все они с треском провалились:


В правом верхнем текстбоксе читаю сгенерированное моим кодом сообщение об ошибке:

Bushman.CAD.UnitTests.Tests.LocalizedProduct_TryParse_Check_BoolValue(null,False):
System.TypeInitializationException : Инициализатор типа "Bushman.CAD.LocalizedProduct" выдал исключение.
  ----> System.IO.FileNotFoundException : C:\Users\developer\AppData\Local\Temp\nunit20\ShadowCopyCache\5128_635702471073123992\Tests_27930185\assembly\dl3\7e84bc6f\b85de2a2_d6a9d001\cad.data.xml

Я подсветил красным цветом ключевую информацию. Это исключение происходит потому, что NUnit копирует тестируемую DLL в произвольно сгенерированный каталог и уже оттуда загружает её и запускает присутствующие в ней тесты. Но скопировав DLL, NUnit не сделал того же самого и для моего XML файла, в виду чего и происходит обозначенное исключение. Обозначенное поведение в NUnit называется Shadow Copy (Теневое Копирование).

Shadow Copy можно включать или отключать в настройках NUnit: меню Tools -> Settings... -> Test Loader -> Advanced -> Shadow Copy -> Enable Shadow Copy:


Обратите внимание на информацию, которую я выделил красным цветом.

Если обозначенную галочку снять, то после перезапуска NUnit теневое копирование не происходит и DLL загружается из того каталога, где она изначально находилась. Соответственно успешно находится и XML файл:




ReportUnit на смену NUnitOrange

Отличная новость:  Anshoo Arora создал новое приложение: ReportUnit, которое является продвинутой заменой NUnitOrange. Теперь на основе XML файлов, генерируемых платформами NUnit, Gallio и MSTest можно генерировать единообразно и весьма удобно оформленные HTML отчёты о результатах тестирования (в т.ч. и сводный).

Принцип использования  ReportUnit тот же самый, что и у NUnitOrange. Скачать ZIP архив с приложением можно отсюда. Web-страничка проекта находится здесь. Исходный код проекта доступен на GitHub.

Ну и, собственно, небольшой скрин, демонстрирующий оформление результатов модульного тестирования:


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

Шаблон проекта VS для написания модульных тестов для .Net-расширений AutoCAD

Ранее я уже приводил пример создания общего шаблона для .NET плагина под любую версию AutoCAD не старше чем 2009-я. Аналогичный шаблон можно создать и для модульных тестов под эти плагины. 

В качестве платформы тестирования для управляемых расширений [плагинов] AutoCAD можно использовать Gallio или NUnit

Gallio благополучно работает с любой версией AutoCAD новее чем 2008 (я не проверял для версий старее чем AutoCAD 2009-й). Однако разработка Gallio на сегодняшний день приостановлена. Тем не менее его можно успешно продолжать использовать. Исходники Gallio опубликованы на GitHub и доступны для изучения\изменения. Однако Gallio работает только с acad.exe - использовать accoreconsole.exe не удастся.

NUnit успешно работает начиная с AutoCAD 2011 и во всех более новых версиях. Версии AutoCAD 2011 - 2014 требуют предварительной установки переменной NEXTFIBERWORLD в значение 0 с последующим перезапуском AutoCAD. По завершению работы тестов, не забудьте переменной NEXTFIBERWORLD снова назначить в качестве значения 1.

Начиная с AutoCAD 2015 компания Autodesk уходит от использования фиберов, поэтому версии AutoCAD, новее чем 2014-я не требуют предварительного изменения обозначенной переменной для успешной работы тестов. Поскольку версии AutoCAD 2010 и все более старые не имеют переменной NEXTFIBERWORLD, то использовать в них тесты NUnit не представляется возможным.

В отличие от Gallio, NUnit может работать как с acad.exe, так и с accoreconsole.exe.

Т.о. в AutoCAD 2009 и 2010 следует использовать тестовую платформу Gallio, в то время как начиная с AutoCAD 2011 можно использовать либо Gallio, либо NUnit по вашему желанию (я предпочитаю NUnit).

Один и тот же исходный код модульных тестов может компилироваться под разные платформы тестирования. По аналогии с шаблоном плагинов AutoCAD, который я демонстрировал в видео ранее, можно создать шаблон для модульных тестов управляемых расширений AutoCAD. Такой шаблон позволяет пакетно компилировать один и тот же исходный код модульных тестов под разные версии AutoCAD с использованием как платформы Gallio, так и платформы NUnit. В приведённом ниже примере для AutoCAD 2009 и 2010 я генерирую тесты с использованием Gallio, а для AutoCAD 2011-2015 - с использованием NUnit. Оба проекта построены на основе соответствующих шаблонов: первый - на основе шаблона для .NET расширений AutoCAD, а второй - на основе шаблона для создания модульных тестов для .NET расширений AutoCAD.

Дополнительно генерируется набор BAT-файлов, каждый из которых предназначен для запуска тестов в конкретной версии AutoCAD. Результаты тестирования оформляются в виде отчёта в формате HTML. На мой взгляд всё получается достаточно удобно.

Когда-то я писал о баге, присутствующем в AutoCAD .NET API и давал свой вариант обходного решения. Обозначенное ниже видео построено на основе этого кода: тесты выявляют баг в API от Autodesk, а так же проверяют работоспособность моего варианта решения.


UPD:
Добавил генерацию BAT-файла, который последовательно выполняет тесты во всех нужных версиях AutoCAD (в данном случае AutoCAD 2009-2015). Вот видео на эту тему:



Далее привожу код тестов данного видео:

/* © Andrey Bushman, 2015
 * Tests.cs
 */
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Diagnostics;
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.CAD.ExtensionMethods.UnitTests;

namespace Bushman.CAD.Extensions.CAD.ExtensionMethods.UnitTests {
  [Fw.TestFixture,
#if NUNIT
 Fw.Apartment(ApartmentState.STA)
#endif
]
  public class Tests {

    const String blockName = "TEMP_BLOCK";

    // ***********************************************************************
    
    [Fw.Test]
    [Fw.Category("Autodesk API")]
    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.Ignore("We can't fix this bug, because it is by Autodesk.")]
    [Fw.Test]
    [Fw.Category("Autodesk API")]
    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")]
    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")]
    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;
    }
  }
}
#endif