#mspec
#mspec
Вопрос:
Я люблю mspec. Это отлично подходит для предоставления ключевых примеров таким образом, чтобы было легко общаться с нетехническими людьми, но иногда я нахожу, что это обеспечивает ненужную многословность, в частности, взрыв классов.
Возьмем следующий пример.
Я хочу смоделировать движение фигуры «конь» в шахматах. Предполагая, что конь не находится рядом с какой-либо другой фигурой или границами доски, существует 8 возможных ходов, которые может иметь конь, я хочу рассмотреть каждую из этих возможностей, но, честно говоря, я слишком ленив, чтобы писать 8 отдельных спецификаций (8 классов). Я знаю, что могу быть умным с поведением и наследованием, но поскольку я хочу охватить 8 допустимых ходов, я не вижу, как я могу сделать это с 8 because
секундами, поэтому 8 отдельных классов.
Каков наилучший способ охватить эти сценарии с помощью mspec?
Некоторый код.
public class Knight
{
public string Position {get; private set;}
public Knight(string startposition)
{
Position = startposition;
}
public void Move
{
// some logic in here that allows a valid move pattern and sets positions
}
}
Что я мог бы сделать.
[Subject(typeof(Knight),"Valid movement")]
public class when_moving_the_knight
{
Establish that = () => knight =new Knight("D4");
Because of = ()=> knight.Move("B3");
It should_update_position = ()=> knight.Position.ShouldEqual("B3");
It should_not_throw;
/// etc..
}
Но не 8 раз.
Комментарии:
1. Я думаю, что вы могли бы захотеть предоставить дополнительную информацию о вашем проекте для кого-то, кто пытается ответить на ваш вопрос надлежащим образом. С моей точки зрения, я бы протестировал это с одним предложением «Because» (потому что the_knight_is_in_e4), за которым следуют 8 разных предложений «It» (It should_be_able_to_move_to_f6 и т.д.), Но я не знаю, применимо ли это в вашем случае. Мне кажется странным, что вам нужно 8 разных контекстов, возможно, вашим классам или вашим тестам может потребоваться какой-то рефакторинг.
2. @brizio проблема, с которой я сталкиваюсь
because knight_is_at_e4
, заключается в том, что я не знаю, куда теперь переместить. Я не могу сделать это вit
, как я утверждаю там3. Я могу ответить со своей точки зрения. Меня волнует взрыв класса, потому что писать его неинтересно. Такое ощущение, что это слишком большой налог sytax, просто чтобы порадовать компилятор. Одна вещь, которую я понимаю, заключается в том, что контекст / спецификацию другим разработчикам обычно сложнее прочитать и понять намерение (по сравнению с заданным when then). Я думаю, что взрыв класса действительно может быть проще для чтения новыми разработчиками по сравнению с итерацией структуры данных. Но я склонен отдавать предпочтение последнему, потому что в нем меньше нажатий клавиш и он более лаконичный. Что касается удобочитаемости выходных данных, я считаю, что любой из них должен выдавать легко читаемый вывод.
4. По моему опыту, лучше оптимизировать для удобства чтения / сканирования кода, чем для «удовольствия от написания», «нажатия клавиш» или «краткости». Другими словами, я бы счел плохой идеей отказываться от удобочитаемости / сканируемости для любой из тех вещей, которые вы предпочитаете.
5. Я просто задал вопрос, потому что было использовано слово «взрыв класса». Если бы он использовал слово «тестовый взрыв», я не знаю, кого бы это волновало. Все дело в контексте. Если у вас есть много сценариев для тестирования, у вас есть много тестов. Конечно, вы могли бы использовать RowTest или table test, чтобы уменьшить это число, но тогда я бы рекомендовал использовать фреймворк, предназначенный для этого. MSpec не является. NSpec позволяет это, но за этим немного сложно следить. Конечно, тесты строк всегда немного сложны для выполнения по своей природе. Как оказалось, есть совершенно верный способ использовать MSpec в этом сценарии. Смотрите ниже.
Ответ №1:
Честно говоря, я не смог бы подсказать вам лучший способ сделать это в MSpec. Но я столкнулся с аналогичной проблемой взрыва класса с MSpec при использовании его в аналогичных обстоятельствах. Я не знаю, пробовали ли вы когда-либо RSpec. В RSpec контексты и спецификации создаются в пределах исполняемого кода. Это означает, что вы можете создать структуру данных, выполнить итерацию по ней и создать несколько контекстов и спецификаций, используя один блок кода. Это становится особенно удобным, когда вы пытаетесь указать, как ведет себя что-либо, основанное на математике (простые множители, крестики-нолики, шахматы и т.д.). Для каждого элемента набора заданных и ожидаемых значений может быть задан единый шаблон поведения.
Этот пример написан в NSpec, контекстной / специальной платформе для C #, созданной по образцу RSpec. Я намеренно оставил неудачную спецификацию. Я просто прошел по этому ката достаточно далеко, чтобы найти место для использования итерации. Неисправная спецификация вынуждает вас устранять недостатки наивной реализации.
Вот еще один пример ката с простым коэффициентом:http://nspec.org/#dolambda
Вывод:
describe Knight
when moving 2 back and 1 left
when a knight at D4 is moved to B3
knight position should be B3
when a knight at C4 is moved to A3
knight position should be A3 - FAILED - String lengths are both 2. Strings differ at index 0., Expected: "A3", But was: "B3", -----------^
**** FAILURES ****
describe Knight. when moving 2 back and 1 left. when a knight at C4 is moved to A3. knight position should be A3.
String lengths are both 2. Strings differ at index 0., Expected: "A3", But was: "B3", -----------^
at ChessSpecs.describe_Knight.<>c__DisplayClass5.<when_moving_2_back_and_1_left>b__4() in c:UsersmattDocumentsVisual Studio 2010ProjectsChessSpecsChessSpecsdescribe_Knight.cs:line 23
2 Examples, 1 Failed, 0 Pending
Код:
using System.Collections.Generic;
using NSpec;
class describe_Knight : nspec
{
void when_moving_2_back_and_1_left()
{
new Each<string,string> {
{"D4", "B3"},
{"C4", "A3"},
}.Do( (start, moveTo) =>
{
context["when a knight at {0} is moved to {1}".With(start,moveTo)] = () =>
{
before = () =>
{
knight = new Knight(start);
knight.Move(moveTo);
};
it["knight position should be {0}".With(moveTo)] = () => knight.Position.should_be(moveTo);
};
});
}
Knight knight;
}
class Knight
{
public Knight(string position)
{
Position = position;
}
public void Move(string position)
{
Position = "B3";
}
public string Position { get; set; }
}
Ответ №2:
Просто используйте его так, как вы хотите. Он должен быть способен перемещаться отсюда туда, он должен быть способен перемещаться отсюда (2) туда (2) и т.д. Очень распространенный шаблон в rspec, но не настолько в MSpec, потому что им обычно злоупотребляют, поэтому никто никогда не говорит об этом, опасаясь направить по ложному пути. Хотя это отличное место для использования. Вы описываете поведение хода коня.
Вы можете описать это еще лучше, если будете более конкретны в своем Its. Он должен быть способен перемещаться на два вверх и вправо, он должен быть способен перемещаться на два вверх и влево. Он не должен иметь возможности перемещаться на дружественный фрагмент и т.д.
Да, вам нужно будет вставить в ваш Ит более одной строки кода, но это нормально. По крайней мере, на мой взгляд.
Комментарии:
1. При таком подходе, Аарон, мне придется сбросить состояние в моем its. Это похоже на искажение шаблона Install, потому что это (AAA) шаблон
2. MSpec не является фреймворком AAA. Это структура контекста / спецификации с конструкциями для написания тестов AAA, если они подходят. Ваш контекст — это шахматная фигура на шахматной доске, и вы указываете или наблюдаете, что она может перемещаться с этого места на это, с другого места на то другое место и т.д. В конечном счете, лучше всего просто не быть слишком педантичным в отношении идей типа AAA и просто делать то, что имеет смысл и легко следовать. Если это означает сброс состояния в каждом It (и / или использование [SetupForEachSpecification]), тогда сделайте это.
Ответ №3:
Из того, что я вижу, в вашем дизайне указано, что Knight выдаст исключение, если переместится в недопустимую позицию. В этом случае я думаю, что у вашего метода есть две разные обязанности: одна для проверки допустимого хода, а другая для выполнения правильного хода или выброса. Я бы предложил разделить ваш метод на две отдельные обязанности.
Для этого конкретного случая я бы извлек метод для проверки того, является ли перемещение допустимым или нет, а затем вызвал бы его из вашего метода перемещения. Что-то вроде этого:
public class Knight
{
internal bool CanMove(string position)
{
// Positioning logic here which returns true or false
}
public void Move(string position)
{
if(CanMove(position))
// Actual code for move
else
// Throw an exception or whatever
}
}
таким образом, вы могли бы протестировать логику внутри canMove для проверки допустимых позиций для данного коня (что вы можете сделать с помощью одного тестового класса и разных «It»), а затем выполнить только один тест для метода Move, чтобы увидеть, завершается ли он ошибкой при задании недопустимой позиции.