Popular Posts

Показаны сообщения с ярлыком test. Показать все сообщения
Показаны сообщения с ярлыком test. Показать все сообщения

суббота, 11 февраля 2012 г.

BDD в javascript. Jasmine

Сегодня я начну серию постов о тестировании Javascript кода. Многие из разработчиков уделяют этому мало времени, считая, что это не стоит внимания, потому что javascript -- это детский язык, который можно выучить за пару дней. Это конечно же ошибка, чтобы понять суть javascript  у некоторых уходит достатточно много времени,  и они крайне удивляются особенностям javascript, например:
 "1" + 3 == 13 // true
 "10" - 3 == 7 // true
Я напишу несколько постов сначала jasmine, затем фреймворк для Prototypejs & scriptaculous. В серии jasmine будет сначала просто работа с jasmine, потом работа с ajax & DOM, потом интерграция c prototypejs и jquery. Jasmine из Ruby, Потом уже Unit тесты от Томаса Фукса (script.aculo.us)
Но это совсем не значит, что я не буду писать о другие посты.
и так поехали.

Jasmine -- это BDD фреймворк для тестирования javascript кода. Довольно простой, не зависит от других фреймворков, то есть был написан для javascript кода, а не для prototype\jquery\yui.

Синтаксис.


Jasmine унаследовало или скорее поддерживает синтаксис основного bdd фреймворка -- RSpec. Тесты состоят из двух частей describe() -- указывает поведение, какой-то общий контекст для которого выполняются тесты. Вторая часть -- it(), простое утверждение. Describe() и it() принимают два аргумента, первый -- это строка, которая показывае, что вы делаете. Второй -- это функция, непосредственно тест.

Проект

Допустим мы скачали jasmine с сайта. Теперь посмотрим какова структура проекта (директорий).
- jasmine-standalone
   -- lib
      --- jasmine-1.1.0
           ---- некоторые файлы
  -- src
     -- //ваши файлы тестируемые
 -- spec
     -- // ваши тесты
spec_runner.html //запуск тестов, в этот файл скопируйте просто отсюда и пропишите свои пути к файлам тестов и своего кода.

Вот достаточно просто. Для запуска тестов просто открываем spec_runner.html в  любом браузере. Смотрим какие прошли, какие не прошли и исправляем.

Написание тестов

Но прежде, чем тестировать нужно что-то написать для тестов. У меня есть вполне реальный пример кода, который я использую, это определитель високосного года.
function isLeapYear(year) {

       if (year % 4 == 0) {
                if (year % 100 == 0) {
                        if (year % 40 == 0) {
                                return true;
                       }
                     return false;
                }
                return true;
       }
      return false;
}; 

Работа понятна принимает значение года, возратит true или false, в зависимости от принятого года.

Вот теперь можно и писать тесты:
describe("isLeapYear", function() {
    it("2004 should be leap year", function() {
        expect(isLeapYear(2004)).toBeTruthy();
        expect(isLeapYear(2004)).toEqual(true);
    });
   it("2000 should be leap year", function() {
      expect(isLeapYear(2000)).toBeTruthy();
   });
   it("1700 should not be leap year", function() {
      expect(isLeapYear(1700)).toBeFalsy();
   });
   it("2001 should not be leap year", function() {
     expect(isLeapYear(2001)).toBeFalsy();
   });
});
Как видим в describe я указал, что будут тесировать функцию isLeapYear, хотя тут можно было указать что угодно. А потом идут утверждения it(). В блоке которых я проверяю с expect(), за expect() идёт некотрое утверждение (matchers), которых несколько в jasmine. Всё довольно просто. Но наши тесты не могут быть полными, ведь я могу отправить вместо числа строку или массив или ничего, поэтому улучшаем функцию.
function isLeapYear(year) {
   if (typeof(year) != 'number')
           throw 'Year should be Number';
 ...
}
Если тип аргумента не номер, будет брошено исключение.
Дописываем необходимые тесты:
it("Not_Number should throws exception", function() {
    expect(function() { isLeapYear('Not_Number'); }).toThrow('Year should be Number');
    expect(function() { isLeapYear(); }).toThrow('Year should be Number');
});
Всё отлично работает:

beforeEach & afterEach

Сейчас рассморим работу только beforeEach, так как afterEach будет  работать также. Сейчас я напишу некоторую функция-класс, которая будет делать, то же что и isLeapYear:
var Year = function() {
    this.isLeapYear = function(year) {
            return isLeapYear(year);
    };   
};
Видим класс возвращает просто функцию isLeapYear.
Напишем тесты, но уже  с beforeEach:
describe("Object Year.isLeapYear", function() {

   beforeEach(function () {
          Obj = new Year();
     });

     it("2004 should be leap year", function() {
     expect(Obj.isLeapYear(2004)).toBeTruthy();
   });
});
В beforeEach устанавливаем Obj, который будет экземпляром Year. А в блоке it(), проверяем метод этого объекта Obj.isLeapYear(). Я не писал все тесты, потому что они будут подобны прошлым. Вот результат:

Пишем свой mathcer

Написание свойго утверждения в jasmine довольно просто. Может возникнуть зачем новый mather? Всё довольно просто -- это устраняет дублирование кода, а также улучшает кода, ведь не нужно писать большие манипуляции, а достаточно написать один раз новый matcher.
Matcher -- это функция, которая будет принимать сколько угодно аргументов (всё зависит от того, что вы проверяете) и манипулирует переменной this.actual -- это поступает из expect() блока. Matcher добавляется с помощью this.addMatchers({}) в блоке beforeEach(), возвратить ваш matcher должен либо true, либо false, и так когда есть немного теории.
beforeEach(function() {
    this.addMatchers({
         toBeLeapYear: function () {
            var year = this.actual;

                       if (typeof(year) != 'number')
                          throw 'Year should be Number';

                      if (year % 4 == 0) {
                      if (year % 100 == 0) {
                         if (year % 40 == 0) {
                             return true;
                         }
                         return false;
                      }
                      return true;
                   }
                   return false;
                  } 
    });
});
Как видим -- это наша старая функция isLeapYear().
Теперь спеки:
it("2004 should be leap year", function() {
    expect(2004).toBeLeapYear();
    expect(2000).toBeLeapYear();
});
it("2001 fail test", function() {
    expect(2001).toBeLeapYear();
});

Здесь один тест не пройдёт, что и ожидалось.

---------------------
Что не было рассмотрено: шпионы и асинхронные спеки









четверг, 9 февраля 2012 г.

Немного о BDD

   Сейчас совсем короткий пост о BDD.
   Что подрузомевает под собой эти 3 буквы)))
  Всё просто, как всегда -- это техника разработки программ, буквально  разработка программ по их поведению.
   Другая техника -- это TDD, вот как можно описать TDD:
  С чем это можно сравнить, это можно сравнить с тем, как идёшь в темноте в незнакомом
месте. Ударился -- это плохо (тесты не прошли), повторно, здесь уже не ударишься (рефакторинг).
Снова повторно проходим, так до тех пор пока не выйдем (тесты все зелёные).
  BDD же -- это
- установка целей, то есть, что будем делать.
- начинаем писать
- пишем примеры к коду, к каждому методу, буквально к каждой строчке -- это и будет
поведение
- автоматизация примеров.
   Другими словами, если использовать ту же аналогию тёмной комнаты.
- цель -- выйти
- начинаем идти, тут есть за что зацепиться и упасть
- падаем\не_падаем, показывая, где упадём, а где нет.

   Время для примера, после стольких слов.  
  Но сначала, замечание. Сейчас я напишу класс, который в зависимости от количества переданых сторон будет показывать -- это треугольник, квадрат, или пятиугольник. Если количество переданых сторон меньше 3 или больше 5, то будет бросаться исключение. Здесь я не буду рассматривать такие зависимости, что, например в треугольнике сумма двух сторон должна быть больше третье, или что у стороны не может быть нулевой длины, или что у квадрата все стороны равны. 


#example.rb
#включаем в файл необходимую нам библиотеку (RSpec)
require 'rspec'

#сначала класс исключений:

class ShapeExeption < Exception 
end

#сам класс для фигур

class Shape
     attr_reader :iam

     TRIANGLE = "Triangle"
     QUADRATE = "Quardate"
     PENTAGON = "Pentagon"

     def initialize(*args)
         @iam = case args.size
               when 3
                   TRIANGLE
               when 4
                  QUADRATE
               when 5
                  PENTAGON
           end   
         raise ShapeExeption.new "What is this shape?" if @iam.nil?         
      end
end

----------------------------------------------------------------------------
   Сейчас поговрим про этот класс. 
  Вначале я определил, что переменная @iam будем только читаться, но пеопределить её нельзя.
   Затем определил констатны, для треугольника\квадрата\пятиугольника.
  Потом метод инициализации, который принимает какое-то количество аргументов, это показывается *. 
   Потом в блоке case...end определяем, длину полученых аргументов. Ну и для каждой длины своя констатнта, которая укажет, какая фигура.
  В конце метода, есть вызов исключения ShapeException, это когда @iam не получена, в принципе, это же можно было бы и включить в блок case, но я не стал.
  А теперь проверим поведение всего этого кода.

describe "Shape" do

    it "should be Triangle with three arguments" do
          shape = Shape.new(1, 2, 3)
          shape.should be_an_instance_of(Shape)
          shape.iam.should eq(Shape::TRIANGLE)
   end

   it "should be Quardate with four arguments" do
         shape = Shape.new(1, 2, 3, 4)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::QUADRATE)
   end

   it "should be Pentagon with five arguments" do
         shape = Shape.new(1, 2, 3, 4, 5)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::PENTAGON)
   end

    context "Exeption" do
        it "should raise ShapeExeption if arguments not equal [3,4,5]" do
            expect { Shape.new }.to raise_error(ShapeExeption)
            expect { Shape.new(1,2,3,4,5,6) }.to raise_error(ShapeExeption)
        end
     it "should raise NoMethodError when write @iam" do          shape = Shape.new(1, 2, 3)          expect { shape.iam = "abc" }.to raise_error(NoMethodError)      end   end end
   Первой строчкой describe Shape, показывается, что будем проверено поведие класса. 
   Затем идут три метода it(), в каждом проверяется, что наша переменная shape инициализирована  от класса Shape, и проверяем, что возвратит метод iam.
   Потом в контектсе (context) проверяем исключения.
У нас их две, когда значений передано меньше или больше, и когда пытаемся перезаписать @iam.
Это проверяется в блоках expect{}.to raise_error().
   Запускаем:

rspec example.rb --color

.....

Finished in 0.00201 seconds
5 examples, 0 failures

   Вот таким несложным образом было проверяно поведение. 
   Да это программа немного надумана, но отражает действительную суть. Этот класс может быть использован для возврата соответвенно объектов Треугольник\Квадра\Пятиугольник\N-угольник.