Често срещани грешки в четвърта задача

  1. Срокът за четвърта задача свърши. Решенията са проверени, тестовете са пуснати и може да видите нашите тестове тук: https://github.com/fmi/ruby-homework/blob/master/tasks/04/original_solution.rb

    В тази тема ще "изсипя" най-често срещаните проблеми. Тези, които предадоха до 23:30 в четвъртък също са получили и inline коментари от мен.

    Всички прилики с решения на студенти са напълно случайни.

    0. Променливи извън тестовете

    describe '#>' do
      bigger = Version.new("1.0.1.6")
      smaller = Version.new("1.0.0.8")
      dup_of_smaller = Version.new(smaller)
    
      it 'returns false when called with <smaller> and <bigger> argument' do
        expect(smaller).to be < bigger
      end
    end
    

    Горното е лошо, защото ако един тест промени някой от обектите, следващите тестове също ще фейлнат. Ето пример:

    a_string = 'test'
    
    it 'it contains test' do
      a_string.sub!('test', '')
    end
    
    it 'is now broken' do
      expect(a_string).to eq 'test'
    end
    

    Освен това, всякакви mock-ове и stub-ове няма да важат за тези променливи.

    1. let-овете на смъртта

    let(:square_numbers) { [1, 4, 9, 16] }
    let(:version_square) { Version.new(square_numbers.join('.')) }
    let(:major_range) { Version::Range.new('1', '2') }
    let(:invalid_version_msg) { 'Invalid version string' }
    let(:snapshot_version) { '1.0-SNAPSHOT' }
    let(:beta_version) { '1.0-BETA' }
    let(:negative_version) { '1.-1' }
    let(:positive_version) { '1.+1' }
    let(:underscore_version) { '1.1_000' }
    let(:missing_major_version) { '.3' }
    let(:missing_minor_version) { '0..3' }
    let(:missing_build_version) { '3.2.15.' }
    
    # 250 реда по-късно
    
    expect(major_range.include?('2')).to eq false
    

    Всеки път, когато ми се наложи да скролна нагоре, за да разбера един тест, значи нещо в този тест е страшно объркано. Когато трябва да скролна 250 реда, за да разбера един ред - дяволчето на едното ми рамо ме кара да вземам точки. :)

    Защо не директно expect(Version::Range.new('1', '2').include?('2')).to eq false или, по-добре, expect(Version::Range.new('1', '2')).to include '2'. Няма нужда от контекст, за да се прочете.

    2. Описания

    it '#>' do
    it 'basic versions' do
    

    vs

    it 'can compare versions using #>' do
    it 'returns components for basic versions' do
    

    Най-важното правило за описанията на it е, че трябва да се чете като изречение. Цялото. Изречението It basic versions не прави никакъв смисъл и не ни е полезно с нищо. То прости версии.

    3. Прекалено ситни тестове

    Сигурно сте чували от някой, че тестовете трябва да съдържат само един expect. Това, както всичко останало, трябва да приемете с въпросите Защо? и Кога?.

    Вземете този код:

    describe '#>' do
      context 'when versions have equal length' do
        it 'compares versions and returns true' do
          expect(Version.new('1.3')).to be > Version.new('1.2')
        end
    
        it 'compares versions and returns false' do
          expect(Version.new('1.2')).to_not be > Version.new('1.3')
        end
    
        it 'compares equal versions and returns false' do
          expect(Version.new('1.2')).to_not be > Version.new('1.2')
        end
      end
    
      context 'when versions have different length' do
        it 'compares versions and returns true' do
          expect(Version.new('1.3')).to be > Version.new('1.2')
        end
    
        it 'compares versions and returns false' do
          expect(Version.new('1.2')).to_not be > Version.new('1.3')
        end
    
        it 'compares equal versions and returns false' do
          expect(Version.new('1.2')).to_not be > Version.new('1.3.0')
        end
      end
    end
    

    Същото може да се напише и така:

    describe '#>' do
      it 'compares versions of equal length' do
        expect(Version.new('1.3.1')).to be > Version.new('1.2')
        expect(Version.new('1.2')).to_not be > Version.new('1.3.1')
        expect(Version.new('1.2')).to_not be > Version.new('1.2')
      end
    
      it 'compares versions of different length' do
        expect(Version.new('1.3.1')).to be > Version.new('1.2')
        expect(Version.new('1.2')).to_not be > Version.new('1.3.1')
        expect(Version.new('1.2')).to_not be > Version.new('1.3.0')
      end
    end
    

    Твърдя, че вторият код се разбира по-лесно и е по-компактен.

    Напълно ОК е да имате повече от един expect в тест, стига да тестват една и съща логика. В този случай, тази логика е сравнението с >.

    4. eq vs match_array - мислете и проверявайте

    expect(Version.new('1.3.15.6').components).to match_array [1, 3, 15, 6]
    

    Каква е разликата между eq и match_array? Този въпрос го зададох на поне 4 човека в коментарите из решенията. Никой не ми отговори.

    На лекцията в понеделник, Сашо ви каза да не забравяте, че съществува match_array. Това не означава "Използвайте match_array винаги и за всичко". Означава "Запомнете какво прави match_array и го използвайте когато трябва". Кога трябва? Когато е валидно елементите на масива да са в разбъркан ред.

    Ако Version.new('1.3.15.6').components върне [6, 15, 3, 1], този тест пак ще мине. Тоест, не си е свършил единствената задача. Когато използвате метод - винаги проверявайте какво прави.

    5. Излишни тестове

    it 'doesn`t raise error' do
      expect { Version.new(Version.new('1.2.3')) }.to_not raise_error
      expect { Version.new('1.2.3') }.to_not raise_error
      expect { Version.new('1.2.0') }.to_not raise_error
      expect { Version.new('') }.to_not raise_error
      expect { Version.new('0') }.to_not raise_error
      expect { Version.new }.to_not raise_error
    end
    
    # ...
    
    it 'converts to string' do
      expect(Version.new('1.1.0').to_s).to eq '1.1'
      expect(Version.new('1.1').to_s).to eq '1.1'
      expect(Version.new('').to_s).to eq ''
      expect(Version.new.to_s).to eq ''
      expect(Version.new(Version.new('1.2.3')).to_s).to eq '1.2.3'
    end
    

    Ако се счупи първия тест => ще се счупи и втория. Тоест, можем да махнем първия и пак ще разберем, че има проблем. Първият тест е излишен код, който не ни дава нищо полезно като информация.

    Още един пример:

    it 'includes first and exludes last' do
        range = Version::Range.new(Version.new('1.1.0'), Version.new('1.2.2'))
        expect(range.to_a).to include('1.1')
        expect(range.to_a).not_to include('1.2.2')
    end
    
    it 'lists all versions' do
      expect(Version::Range.new('1.1.0', '1.2.2').to_a).to eq [
        '1.1', '1.1.1', '1.1.2', '1.1.3', '1.1.4', '1.1.5',
        '1.1.6', '1.1.7', '1.1.8', '1.1.9', '1.2', '1.2.1'
      ]
    end
    

    Първият тест очевидно е вързан за втория. По същата логика, той е напълно излишен и не допринася с нищо.

    6. Излишни променливи и лоши имена

    it 'checks if version1 > version2' do
      version1 = Version.new("2.4")
      version2 = Version.new("2.3")
      expect(version1).to be > version2
      version1 = Version.new("0.2.4")
      version2 = Version.new("0.2.3")
      expect(version1).to be > version2
      version1 = Version.new("1.0.0")
      version2 = Version.new("1")
      expect(version1).to be > version2
      version1 = Version.new("0.2.4")
      version2 = Version.new("0.2.4")
      expect(version1).to be >= version2
      version1 = Version.new("2.4")
      version2 = Version.new("2.4")
      expect(version1).to be >= version2
    end
    

    Защо това, а не това:

    it 'checks if version1 > version2' do
      expect(Version.new("2.4")  ).to be >  Version.new("2.3")
      expect(Version.new("0.2.4")).to be >  Version.new("0.2.3")
      expect(Version.new("1.0.0")).to be >  Version.new("1")
      expect(Version.new("0.2.4")).to be >= Version.new("0.2.4")
      expect(Version.new("2.4")  ).to be >= Version.new("2.4")
    end
    

    Кое е по-ясно?

    Ето още един пример:

    it 'can compare with >=' do
      version1 = Version.new('1.3.1')
      version2 = Version.new('1.3.1')
      version3 = Version.new('1.2.3')
      version4 = Version.new('1.4')
      expect(version1).to be >= version3
      expect(version3).to be >= version1
      expect(version1).to be >= version2
      expect(version4).to be >= version1
    end
    

    Тук на мозъкът ми му е трудно да match-ва всички променливи, една по една, накръст.

    7. Тестване, че нещо не може да промени обекта

    Ако в нашата имплементация на #components нямаше .dup, щеше да се случва това:

    version = Version.new('1.2.3')
    version.components << 4
    
    version.to_s #=> '1.2.3.4'
    

    Очевидно не искаме да може да става така. Защо? Защото този масив можем да го подадем на друг метод и да го променим несъзнателно. Това ще доведе до изключително трудно намиращи се бъгове.

    Това директно може да се напише на тест така:

    version = Version.new('1.2.3')
    version.components << 4
    
    expect(version.to_s).to eq '1.2.3'
    

    8. To be or not to be

    expect(Version.new('2') > Version.new('1')).to be true
    expect(Version.new('2') < Version.new('1')).to be false
    

    Да, горното ще работи. Но съобщението, което ще ви даде ако фейлне ще е изоморфно на expected false to be true. Лекцията от понеделник би трябвало да ви е подсетила за следното:

    expect(Version.new('2')).to be > Version.new('1')
    expect(Version.new('2')).to be < Version.new('1')
    

    Тук, ако фейлне, грешката ще е подобна на expected '2' to be > '1'. Много по-описателно и по-полезно. Разликата между двете е като разликата между if (2 > 1) == true и if 2 > 1. Никога не бихте написали първото, нали?

    9. Проверки за тип на обекта при създаване

    it 'makes instance with no argument' do
      expect(Version.new).to be_an_instance_of(Version)
    end
    

    Създаваме инстанция на класа Version и очакваме тя да е инстанция на класа Version. Почти толкова полезно, колкото expect('a constant string').to eq 'a constant string'. Този тест не прави нищо и не трябва да съществува. Ако някой мисли по друг начин - ще се радвам да обясни защо, за да стигнем до основата на "проблема" :)

    10. Скоби

    (names.split ' ').first
    names.split(' ').first
    

    Кой от горните два реда ви изглежда по-естествено? Втория? Защо тогава това:

    expect((range.include? version)).to be true
    

    се вижда по-често в решенията ви от това:

    expect(range.include?(version)).to be true
    

    Това е Ruby, не пишете на Haskell или Scheme. Не слагайте скоби на случайни места докато не проработи. include? е метод като всеки друг - нормално е скобите да са около аргументите му.

Трябва да сте влезли в системата, за да може да отговаряте на теми.