Тестовый модуль VBA - PullRequest
       43

Тестовый модуль VBA

0 голосов
/ 21 декабря 2018

Я пытаюсь написать тестовый модуль для тестирования одного из модулей, которые я написал в VBA.В частности, у меня есть оператор if, который я хотел бы вызвать с помощью тестового модуля, задавая модулю / функции неправильные начальные параметры.Модуль / функция, которую я хотел бы протестировать:

Function TPR_TNR_FPR_FNR(expected_vals As Range, pred_vals As Range, 
val_tested As Integer) As Double

If WorksheetFunction.CountA(expected_vals) <> 
WorksheetFunction.CountA(pred_vals) Then
   MsgBox "Cells in Expected_vals and pred_vals must be the same in length"
   Stop
End If

count_all = 0
For Each cell In expected_vals
  If cell = val_tested Then
    count_all = count_all + 1
  End If
Next cell

count_correct = 0
For i = 1 To expected_vals.Cells.Count
  If (expected_vals.Cells(i).Value = pred_vals.Cells(i).Value) And 
(expected_vals.Cells(i).Value = val_tested) Then
     count_correct = count_correct + 1
  End If
Next

TPR_TNR_FPR_FNR = count_correct / count_all

End Function

И мой тестовый модуль:

 '@TestModule
 Private Assert As Rubberduck.AssertClass

 '@TestMethod
 Public Sub Test1()
 'Arrange
 Const expected As String = "Cells in Expected_vals and pred_vals must be 
 the same in length"
 Dim actual As String

 'Act
 Dim r1, r2 As Variant
    r1 = 
 WorksheetFunction.Transpose(Application.ActiveSheet.Range("A1:A5").Select)
    r2 = 
 WorksheetFunction.Transpose(Application.ActiveSheet.Range("B1:B4").Select)
 actual = Module1.TPR_TNR_FPR_FNR(r1, r2, 0)

 'Assert
 Assert.AreEqual expected, actual, "Expected MsgBox not received"
 End Sub

Однако я получаю сообщение об ошибке "Несоответствие типа аргумента Byref" для варианта r1когда тестовый скрипт доходит до «фактического = ...».Пожалуйста, помогите мне, я не знаю, что я делаю неправильно.Я уже успешно установил Rubberduck.

Ответы [ 2 ]

0 голосов
/ 21 декабря 2018

Прежде всего, спасибо за тестирование вашего кода VBA.Профессиональные разработчики на каждом языке пишут юнит-тесты, и с Rubberduck (отказ от ответственности: я управляю этим проектом) вы активизируете свою игру и вносите свой вклад, чтобы сделать VBA менее страшным языком .

Однако не весь код поддается тестированию.Чтобы написать модульные тесты для функции, эта функция должна быть написана таким образом, чтобы связывание сводилось к минимуму, а ее зависимости в идеале воспринимались как параметры.

One Thing , который определенно делает функцию непроверяемой, - это когда эта функция включает взаимодействие с пользователем.MsgBox открывает модальное окно, которое необходимо закрыть вручную, поэтому тестируемый код избегает его 1 .Stop - это код отладчика, который не должен быть в рабочем состоянии, а также препятствует выполнению теста.


Вы попали в автобус или отправляетесь для решения новых задач в другом месте, икто-то должен завтра принять этот код.Будут ли они проклинать ваше имя или хвалить вашу работу?

Я не могу прочитать TPR_TNR_FPR_FNR и сразу же выяснить, что он делает, просто по имени.Это проблема, потому что это делает обслуживание намного сложнее, чем нужно: если мы не знаем, что функция должна делать, как мы узнаем, что она делает это правильно?С набором тестов с хорошими именами мы можем знать, как он ведет себя во всех случаях ... при условии использования тестов с хорошими именами.Test1 мало что нам говорит, кроме хорошо, он что-то тестирует .

Сначала отбросьте операторы MsgBox и Stop - вместо этого выведите ошибку в этом предложении guard:

If WorksheetFunction.CountA(expected_vals) <> WorksheetFunction.CountA(pred_vals) Then
    Err.Raise 5, "TPR_TNR_FPR_FNR", "Cells in Expected_vals and pred_vals must be the same in length"
End If

Обратите внимание, что здесь не сравнивается количество строк и / или столбцов каждого диапазона; только то, что у них одинаковое количество непустых ячеек .Только с этим одним оператором Err.Raise я могу написать несколько модульных тестов:

  • При заданных диапазонах одинакового размера с одинаковым количеством непустых ячеек ошибка не выдается.
  • При наличии диапазонов одинакового размера с разным количеством непустых ячеек выдается ошибка 5.
  • При наличии диапазонов разного размера с одинаковым количеством непустых ячеек не выдается ошибка.
  • При наличии диапазонов разного размера с разным количеством непустых ячеек выдается ошибка 5.
  • При наличии несмежных диапазонов с одинаковым количеством непустых ячеек ошибка не выдается.
  • При наличии двух диапазонов без непустых ячеек ошибка не выдается.

Если какое-либо из этих утверждений выглядит неправильно, ваш код работает не так, как задумано -потому что все эти тесты пройдут, учитывая, что выдается ошибка, когда WorksheetFunction.CountA возвращает различное значение для двух диапазонов.

Пройдя предложение guard, функция переходит к итерации ячеек в expected_vals, что имеетсопоставление значенийпараметр val_tested.

Функция работает с Range объектами, перебирает ячейки, неявно сравнивая Range.[_Default] (Value) со значением Integer: если какая-либо из ячеек в expected_vals содержит ошибку, Несоответствие типов здесь выдается ошибка:

If cell = val_tested Then

Поскольку вышеизложенное действительно делает это:

If cell.Value = val_tested Then

Range.Value являетсяVariant, который может содержать любое значение: числовые значения равны Variant/Double, поэтому даже в «счастливом пути» происходит неявное преобразование, чтобы сравнить это Double с предоставленным Integer.Похоже, что val_tested должно быть Double.

Но Range.Value также может быть Variant/Error, и этот вариантный подтип не может сравниваться с любым другим типом без выброса несоответствия типов.Если ожидается выбрасывание этого несоответствия типов, для этого должен быть тест.Иначе, это должно быть обработано - и тогда должен быть тест для этого:

  • Учитывая значение ошибки в expected_vals, выдает ошибку 13 (или нет?)

Если этой ошибки не должно быть, функция должна активно ее предотвратить:

For Each cell In expected_vals
    If Not IsError(cell.Value) Then
        If cell.Value = val_tested Then count_all = count_all + 1
    End If
Next

Таким образом, count_all - это на самом деле количество ячеек в expected_vals, которые имеют значение, которое соответствует предоставленному параметру val_tested: я считаю, что matchingExpectedValuesCount будет более описательным / значимым именем для него, и оно должно бытьобъявлено локально с помощью оператора Dim (инспекции Rubberduck должны предупредить вас об этом ... и еще пару вещей).

Далее у нас есть цикл For, который делает удивительное предположение:

For i = 1 To expected_vals.Cells.Count
    If (expected_vals.Cells(i).Value = pred_vals.Cells(i).Value) And (expected_vals.Cells(i).Value = val_tested) Then

Теперь мы предполагаем очень специфическую форму для предоставленных диапазонов.Если мы добрались до этого с диапазоном в 2 столбца или несмежным диапазоном нескольких областей, это то место, где мы собираемся взорвать.

Оговорка о защите должна защищать от этого предположения,и выбросить ошибку соответственно.WorksheetFunction.CountA / количество непустых ячеек в каждом заданном диапазоне недостаточно для правильной защиты от неверных входных данных.Примерно так должно быть более точно:

If expected_vals.Rows.Count <> pred_vals.Rows.Count _
    Or expected_vals.Columns.Count <> 1 _
    Or pred_vals.Columns.Count <> 1 _
Then
    Err.Raise 5, "TPR_TNR_FPR_FNR", "Invalid inputs"
End If

Теперь предположения будут такими:

  • При заданных диапазонах одинакового размера с одинаковым количеством ячеек ошибка не выдается.
  • При заданных диапазонах одинакового размера с разным количеством ячеек выдается ошибка 5.
  • При заданных диапазонах разного размера с одинаковым количеством ячеек выдается ошибка 5.
  • Данодиапазоны разных размеров с разным количеством ячеек, выдается ошибка 5.
  • При наличии несмежных диапазонов с одинаковым количеством непустых ячеек выдается ошибка 5.
  • с учетом двух диапазоновбез каких-либо непустых ячеек ошибка не выдается.

Теперь, когда это установлено, 2-й цикл также должен обрабатывать Variant/Error для предотвращения несоответствия типов ошибок.

If Not IsError(expected_vals.Cells(i).Value) _
    And Not IsError(pred_vals.Cells(i).Value) _
Then
    If (expected_vals.Cells(i).Value = pred_vals.Cells(i).Value) And (expected_vals.Cells(i).Value = val_tested) Then
        count_correct = count_correct + 1
    End If
End If

Наконец, присвоение результата функции приведет к ошибке деления на ноль, если count_all равно 0:

TPR_TNR_FPR_FNR = count_correct / count_all

Если это ожидается, должен быть проверенЭто.В противном случае оно должно быть защищено, суррогатное значение должно быть возвращено (например, -1 или 0), ... и для него должен быть тест!

  • При отсутствии ячеек в expected_vals соответствует предоставленному val_tested значению, выдается ошибка 11.

или ..

  • При отсутствии ячеек в expected_vals соответствует предоставленному val_tested значению,возвращает 0.

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

Для каждого отдельного маркера "Given ..., ..." выше, тест должен быть написан, чтобы доказать это.В вашем тесте есть ряд уже выявленных проблем , а также ряд неопознанных проблем.

Тайный соус для написания хороших тестов - управление входными данными.Наличие параметров Excel.Range делает его сложнее, чем необходимо: теперь вам нужно иметь тестовый лист с фактическим диапазоном тестов с кучей тестовых значений, ... и это кошмар, потому что теперь от того, пройдут тесты или нет, зависит отвещи, которых нет в самих тестах - и это очень плохо: хорошие тесты должны иметь надежные, воспроизводимые, согласованные результаты.

В этой функции я не видел ничего такого, что говорит о том, что нужно для работы с Range параметрами.На самом деле, работа с простыми массивами сделает его значительно более эффективным и намного проще утверждать предположения в предложении guard - просто проверьте границы массива!Работа с простыми массивами также означает, что тесты теперь могут быть автономными: код установки теста может легко определять тестовые массивы для предоставления функции, тем более что мы установили, что эти массивы должны быть одномерными.

Таким образом, необходимо переписать функцию для работы с массивами Variant.

Как только это будет сделано (я оставлю эту часть вам!), Вы можете легко настроить все необходимые входные данные для всех тестов,и тестовые шаблоны Rubberduck делают это довольно легко.Вот как может выглядеть один из этих тестов:

'@TestMethod
Public Sub GivenDifferentSizeArrays_Throws()
    Const ExpectedError As Long = 5
    On Error GoTo TestFail

    'Arrange:
    Dim expectedValues As Variant
    expectedValues = Array(1, 2, 3)

    Dim predValues As Variant
    predValues = Array(1, 2, 3, 4)

    'Act:
    Dim result As Double
    result = TPR_TNR_FPR_FNR(expectedValues, predValues, 1)

Assert:
    Assert.Fail "Expected error was not raised."

TestExit:
    Exit Sub
TestFail:
    If Err.Number = ExpectedError Then
        Resume TestExit
    Else
        Resume Assert
    End If
End Sub

В этом тесте (обратите внимание, что для изменения функции требуется два варианта массива, а не Range параметров), ожидается, что ошибка 5 будет вызванавызов функции, учитывая два массива разного размера: если ожидаемая ошибка не возникает, тест не пройден.Если это так, тест проходит успешно.

Другой тест может подтвердить, что выдается ошибка 13 с учетом значения ошибки в одной из ячеек - здесь значение #N/A ошибка ячейки:

    'Arrange:
    Dim expectedValues As Variant
    expectedValues = Array(1, 2, 3)

    Dim predValues As Variant
    predValues = Array(CVErr(xlErrNA), 2, 3)

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


Явные предположения

Вот переписанная версия вашей функции, которая делает ее предположения явными идолжно быть намного проще писать тесты:

Public Function TPR_TNR_FPR_FNR(ByRef expected_vals As Variant, ByRef pred_vals As Variant, ByVal val_tested As Double) As Double

    Dim workValues As Variant
    Dim predValues As Variant

    If Not IsArray(expected_vals) Or Not IsArray(pred_vals) Then
        Err.Raise 5, "TPR_TNR_FPR_FNR", "Parameters must be arrays."
    Else
        workValues = expected_vals
        predValues = pred_vals
    End If

    If TypeOf expected_vals Is Excel.Range Then
        If expected_vals.Columns.Count <> 1 Then Err.Raise 5, "TPR_TNR_FPR_FNR", "'expected_vals' must be a single column."
        workValues = Application.WorksheetFunction.Transpose(expected_vals)
    End If

    If TypeOf pred_vals Is Excel.Range Then
        If pred_vals.Columns.Count <> 1 Then Err.Raise 5, "TPR_TNR_FPR_FNR", "'pred_vals' must be a single column."
        predValues = Application.WorksheetFunction.Transpose(pred_vals)
    End If

    If UBound(workValues) <> UBound(predValues) Then
        Err.Raise 5, "TPR_TNR_FPR_FNR", "'expected_vals' and 'pred_vals' must be the same size."
    End If

    Dim matchingExpectedValuesCount As Long
    Dim currentIndex As Long
    For currentIndex = LBound(workValues) To UBound(workValues)
        If workValues(currentIndex) = val_tested Then
            matchingExpectedValuesCount = matchingExpectedValuesCount + 1
        End If
    Next

    If matchingExpectedValuesCount = 0 Then
        TPR_TNR_FPR_FNR = 0
        Exit Function
    End If

    Dim count_correct As Long
    For currentIndex = LBound(predValues) To UBound(predValues)
        If workValues(currentIndex) = predValues(currentIndex) And workValues(currentIndex) = val_tested Then
            count_correct = count_correct + 1
        End If
    Next

    TPR_TNR_FPR_FNR = count_correct / matchingExpectedValuesCount

End Function

Обратите внимание, что я не на 100% ясен в отношении цели, поэтому я оставил несколько идентификаторов, как у вас, - я быТем не менее, настоятельно рекомендуем переименовать их.


1 Функции модульного тестирования Rubberduck включают "подделки" API, который позволяет вам настраивать тест и буквально перехватывать вызовы MsgBox (и несколько других), позволяя вам написать тест для процедуры, которая обычно выдает окно сообщения , даже не отображая его во время выполнения теста .API также позволяет настроить его возвращаемое значение, так что вы можете, например, проверить, что происходит, когда пользователь нажимает «Да», а затем другой тест может подтвердить, что происходит, когда пользователь нажимает «Нет».

0 голосов
/ 21 декабря 2018

Изменить

Application.ActiveSheet.Range("A1:A5").Select

на

Application.ActiveSheet.Range("A1:A5")

Функция TPR_TNR_FPR_FNR (ожидаемые значения в качестве диапазона, pred_vals в качестве диапазона, val_tested в качестве целого числа) в виде двойного

Ожидаемые_валы - это диапазон иpred_vals - Range, но r1, r2 - вариант.

, поэтому происходит несовпадение типов.

...