[RUBY] Comment écrire dynamiquement des cas de test itératifs à l'aide de test / unit (Test :: Unit)

Code de la théorie

Puisque Ruby peut générer du code dynamique même pour la classe et la méthode, il peut être écrit comme suit.

ARR = [true, false, true]
Class.new(Test::Unit::TestCase) do
  ARR.each_with_index do |e, idx|
    define_method "test_#{idx}" do
      assert e
    end
  do
end

Ceci est synonyme d'écriture comme suit.

ARR = [true, false, true]
class AnyTestCase < Test::Unit::TestCase
  def test_0
    assert ARR[0]
  end

  def test_1
    assert ARR[1]
  end

  def test_2
    assert ARR[2]
  end
end

Le code de test est DAMP de DRY

On dit souvent que le code de test devrait se concentrer sur ** DAMP (Phrases descriptives et significatives) ** plutôt que sur DRY (Ne vous répétez pas).

Je suis fondamentalement d'accord avec cette question. Le code de test doit être une "spécification", et dans ce sens, la même expression apparaît plusieurs fois dans une "spécification écrite dans un langage naturel", ce qui est inévitable en raison de la priorité accordée à la clarté. Cependant, il va sans dire que l'équilibre est important dans tout.

Maintenant, le problème ici est "le traitement de vérification pour une grande quantité de données". Par exemple, il y a quelque chose comme ça

Si vous le mettez en œuvre honnêtement

Ciblons le déplacement du blog. Si vous vérifiez honnêtement que le blog vers lequel vous vous déplacez contient plus d'un octet de contenu dans la balise <article>, vous verrez:

require 'test/unit'
require 'httpclient'
require 'nokogiri'
  
class WebTestCase < Test::Unit::TestCase
  def setup
    @c = HTTPClient.new
  end

  def test_0
    doc = Nokogiri::HTML(@c.get("https://example.com/entry/one").body)
    assert_compare 1, "<", doc.css('article').text.length
  end

  def test_1
    doc = Nokogiri::HTML(@c.get("https://example.com/entry/two").body)
    assert_compare 1, "<", doc.css('article').text.length
  end

  def test_2
    doc = Nokogiri::HTML(@c.get("https://example.com/entry/three").body)
    assert_compare 1, "<", doc.css('article').text.length
  end
end

Il est subtil d'écrire ceci jusqu'à def test_1000.

Implémentation répétée avec itérateur

Étant donné que la condition de vérification (assert_compare) ne change pas, je peux penser à un plan pour faire de la liste d'URL un itérateur (tableau) et la tester à plusieurs reprises, mais cela n'est pas recommandé pour la raison décrite plus loin.

require 'test/unit'
require 'httpclient'
require 'nokogiri'

URLS = %w(
  https://example.com/entry/one
  https://example.com/entry/two
  https://example.com/entry/three
)

class WebTestCase < Test::Unit::TestCase
  def setup
    @c = HTTPClient.new
  end

  def test_content_length
    URLS.each do |url|
      doc = Nokogiri::HTML(@c.get(url).body)
      assert_compare 1, "<", doc.css('article').text.length
    end
  end
end

La notation% w est pratique. Eh bien, cela semble bon à première vue. Cependant, le problème se produit en fait lorsque l'assertion échoue. Vous trouverez ci-dessous le résultat de l'exécution, mais ** je ne sais pas quelle URL a échoué **.

$ bundle exec ruby test_using_array.rb
Started
F
=========================================================================================================================================================================================
test_using_array.rb:25:in `test_content_length'
test_using_array.rb:25:in `each'
     24:   def test_content_length
     25:     URLS.each do |url|
     26:       doc = Nokogiri::HTML(@c.get(url).body)
  => 27:       assert_compare 1, "<", doc.css('article').text.length
     28:     end
     29:   end
     30: end
test_using_array.rb:27:in `block in test_content_length'
Failure: test_content_length(WebTestCase):
  <1> < <0> should be true
  <1> was expected to be less than
  <0>.
=========================================================================================================================================================================================

Finished in 0.0074571 seconds.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 tests, 3 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

C'est parce que l'unité du test est chaque méthode (def test_content_length dans l'exemple ci-dessus).

Il vaut mieux laisser Ruby le créer dynamiquement que générer du code

Pour chaque itérateur (tableau), nous voulons créer une méthode pour chaque test et la tester. Après tout, n'y a-t-il pas d'autre choix que de l'implémenter honnêtement en copiant? N'y a-t-il pas d'autre choix que de générer du code avec la macro de Hidemaru? Quand vous en venez à l'idée, j'aimerais que vous vous souveniez de la génération de code dynamique qui est la magie noire (métaprogrammation) de Ruby.

Comme mentionné au début, Ruby peut générer du code de manière dynamique. La classe et la méthode ne font pas exception. Vous pouvez l'utiliser pour créer dynamiquement des méthodes de test une par une pour le contenu de l'itérateur.

URLS = %w(
  https://example.com/entry/one
  https://example.com/entry/two
  https://example.com/entry/three
)

Class.new(Test::Unit::TestCase) do
  def setup
    @c = HTTPClient.new
  end

  URLS.each_with_index do |url, idx|
    define_method "test_#{idx}" do
      doc = Nokogiri::HTML(@c.get(url).body)
      assert_compare 1, "<", doc.css('article').text.length
    end
  end
end

Ce code est équivalent à ce qui suit, qui est identique à l'implémentation simple.

URLS = %w(
  https://example.com/entry/one
  https://example.com/entry/two
  https://example.com/entry/three
)

class AnyTestCase < Test::Unit::TestCase
  def setup
    @c = HTTPClient.new
  end

  def test_0
    doc = Nokogiri::HTML(@c.get(URLS[0]).body)
    assert_compare 1, "<", doc.css('article').text.length
  end

  def test_1
    doc = Nokogiri::HTML(@c.get(URLS[1]).body)
    assert_compare 1, "<", doc.css('article').text.length
  end

  def test_2
    doc = Nokogiri::HTML(@c.get(URLS[2]).body)
    assert_compare 1, "<", doc.css('article').text.length
  end
end

Dans l'exemple suivant, l'emplacement de l'échec est spécifié comme «Échec: test_2 ()».

$ bundle exec ruby test_using_black_magic.rb
Loaded suite test_using_black_magic
Started
..F
=========================================================================================================================================================================================
test_using_black_magic.rb:27:in `block (3 levels) in <main>'
Failure: test_2():
  <1> < <0> should be true
  <1> was expected to be less than
  <0>.
=========================================================================================================================================================================================

Finished in 0.007288 seconds.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3 tests, 3 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
66.6667% passed
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# each_with_index a une origine 0. Un échec dans test_2 signifie un échec lorsque la valeur est ʻARR [2] `, vous devriez donc vérifier cette valeur.

Il est également possible de spécifier avec --name

Si l'ordre des valeurs stockées dans l'itérateur est garanti, il prendra également en charge la spécification avec --name. Par exemple, si vous voulez tester uniquement sur la valeur de ʻARR [1] `, spécifiez comme suit.

$ bundle exec ruby test_multiple.rb --name test_1

Points de mise en œuvre

La génération dynamique de classe avec Class.new et la génération de méthode dynamique avec define_method sont les clés. Si cela vous intéresse, recherchez "Ruby Black Magic".

Attention à ne pas en faire trop

assertion Ceci est valable lorsque les conditions sont uniformes. Au contraire, si l'assertion à appliquer diffère en fonction de la valeur d'entrée (= un branchement conditionnel se produit), il est préférable de l'implémenter honnêtement comme DAMP.

Épilogue

Je suis un amateur dans ce domaine, donc je ne sais pas si cela me convient.

EoT

Recommended Posts

Comment écrire dynamiquement des cas de test itératifs à l'aide de test / unit (Test :: Unit)
JUnit 5: Comment écrire des cas de test dans enum
Comment écrire un test unitaire pour Spring Boot 2
Comment faire un test unitaire de Spring AOP
Je veux écrire un test unitaire!
[SpringBoot] Comment écrire un test de contrôleur
Comment écrire du code de test avec la certification de base
Comment écrire des rails
Comment écrire docker-compose
Comment écrire Mockito
Comment écrire un fichier de migration
Comment écrire du bon code
Comment rédiger un commentaire java
[Refactoring] Comment écrire le routage
Introduction à Micronaut 2 ~ Test unitaire ~
Test unitaire d'architecture avec ArchUnit
Comment écrire Junit 5 organisé
Comment écrire des graines de Rails
Comment écrire le routage Rails
Comment autoriser à l'aide de graphql-ruby
Comment tester avec des images lors de l'utilisation d'ActiveStorage et de Faker
Comment écrire une option de requête lors de l'utilisation de gem ruby-firebase (memorial)
Étudier Java # 6 (Comment écrire des blocs)
[R Spec on Rails] Comment écrire du code de test pour les débutants par les débutants
[Rails] Comment écrire la gestion des exceptions?
Comment écrire une déclaration de variable Java
Comment rédiger un code facile à comprendre [Résumé 3]
J'ai testé comment utiliser le test / l'unité de Ruby et le code de Janken.
[RSpec] Test unitaire (avec gem: factory_bot)
Comment créer CloudStack à l'aide de Docker
Comment exécuter un contrat avec web3j
Comment trier une liste à l'aide du comparateur
Introduire RSpec et écrire le code de test unitaire
[Rails] Comment télécharger des images à l'aide de Carrierwave
[Basique] Comment écrire un auto-apprentissage Dockerfile ②
Bibliothèque de tests unitaires Java Artery-Easy to use
[Java] Comment calculer l'âge à l'aide de LocalDate
Résumé de l'écriture des arguments d'annotation
[Introduction à Java] Comment écrire un programme Java
Écrire du code difficile à tester
Comment tester l'étendue privée avec JUnit
Pour implémenter la publication d'images à l'aide de rails
Comment écrire un spécificateur de coupe de point Spring AOP
Comment insérer des icônes à l'aide de Font awesome