Found such a task in the course on C #:

Implement vending machine in code using TDD approach

There are the following features:

  • You can update product list at any time
  • You can insert coins, get coins back and get remainder
  • You can buy 1 product at once for inserted coins
  • Machine accepts the following coins: 5ȼ, 10ȼ, 20ȼ, 50ȼ, 1 € and 2 €

There is such an interface:

public interface IVendingMachine { /// <summary>Vending machine manufacturer.</summary> string Manufacturer { get; } /// <summary>Amount of money inserted into vending machine. </summary> Money Amount { get; } /// <summary>Products that are sold.</summary> Product[] Products { get; set; } /// <summary>Inserts the coin into vending machine.</summary> /// <param name="amount">Coin amount.</param> Money InsertCoin(Money amount); /// <summary>Returns all inserted coins back to user.</summary> Money ReturnMoney(); /// <summary>Buys product from list of product.</summary> /// <param name="productNumber">Product number in vending machine product list.</param> Product Buy(int productNumber); } public struct Money { public int Euros { get; set; } public int Cents { get; set; } } public struct Product { /// <summary>Gets orsets the available amount of product.</summary> public int Available { get; set; } /// <summary>Gets orsetsthe product price.</summary> public Money Price { get; set; } /// <summary>Gets orsetsthe product name.</summary> public string Name { get; set; } } 

What will the development of tests look like? As far as I understand, you first need to implement a test method and then write it, but I don’t understand how you can write a unit test first, and then implement the method. Never wrote units before, can you help explain and show with an example?

    4 answers 4

    Good job.

    The main thing is that you have a subject area, namely, what you need to be able to fulfill (the requirements and possibilities of the entity).

    In your case, we start by creating a class that implements the interface. While empty:

      public class VendingMachine : IVendingMachine { public Money Amount { get; } public Product[] Products { get; set; } public Money InsertCoin(Money amount) { throw new NotImplementedException(); } public Money ReturnMoney() { throw new NotImplementedException(); } public Product Buy(int productNumber) { throw new NotImplementedException(); } public string Manufacturer { get; } } 

    The first requirement is you can update the product list at any time

    The interface does not implement exactly how it will happen, and therefore the main question for TDD is how it will be convenient for a third-party developer to update the assortment.

    We write a test for this case:

      var machine = new VendingMachine(); machine.AddProduct(); 

    We immediately think about it, but how convenient it is to steer products, because judging by the structure of the product, it sets the quantity \ name and price of the item. I would prefer a simple option:

      var beerPrice = new Money() {Euros = 1, Cents = 10 }; machine.AddOrUpdateProduct("Пиво", beerPrice, 3); 

    Immediately add a method with this signature to the class:

     public void AddOrUpdateProduct(string name, Money beerPrice, int count) { throw new NotImplementedException(); } 

    Now we finish the test so that it checks the case we need:

      var machine = new VendingMachine(); var beerPrice = new Money() {Euros = 1, Cents = 10 }; var count = 3; machine.AddOrUpdateProduct("Пиво", beerPrice, count); Assert.AreEqual(machine.Products.Length, 1); Assert.AreEqual(machine.Products[0].Available, count); 

    Run the test - we get the error:

    The TDD.UnitTest1.UpdateVendingMachineProducts validation method threw an exception: System.NotImplementedException: Method or operation is not implemented ..

    The difficult part of the work is completed, it remains to implement a simple way that will lift the test. Simple, but acting as if the essence exists in the real world, it is not necessary to fence the magic.

    I personally do not like working with arrays, so I’ll hide the list in the implementation, with the result:

     public Product[] Products { get { return products.ToArray(); } set { products = value.ToList(); } } private List<Product> products = new List<Product>(); public void AddOrUpdateProduct(string name, Money beerPrice, int count) { products.Add(new Product() {Available = count, Name = name, Price = beerPrice}); } 

    The test has successfully passed, the first feature is implemented, hurray.


    Second - you can insert coins, get coins back and get remainder

    The description gives an elementary hint of three tests - put in money, returned, received change. The first two can be realized right now, the third - it is better to postpone, the change can only be from the purchase, but not a word about it. Here the interface is set quite well, we write tests on it:

     [TestMethod] public void InsertCoin() { var machine = new VendingMachine(); var inserted = new Money() {Euros = 1}; var returned = machine.InsertCoin(inserted); Assert.AreEqual(machine.Amount, inserted); Assert.AreEqual(returned, inserted); } [TestMethod] public void ReturnMoney() { var machine = new VendingMachine(); var count = 1; machine.InsertCoin(new Money() { Euros = count }); var back = machine.ReturnMoney(); Assert.AreEqual(back.Euros, count); Assert.AreEqual(machine.Amount.Euros, 0); } 

    We realize:

    Money overload, let them add up. While elementary:

     public static Money operator +(Money m1, Money m2) { return new Money() { Euros = m1.Euros + m2.Euros, Cents = m1.Cents + m2.Cents }; } 

    So for the first test:

     public Money Amount { get; protected set; } public Money InsertCoin(Money amount) { Amount = Amount + amount; return Amount; } 

    And for the second:

     public Money ReturnMoney() { var amount = Amount; Amount = new Money(); return amount; } 

    Do not forget about the substantive part - the amount in the machine should change during these operations. Hooray, we have three green test.


    Third case - You can buy 1 product at once for inserted coins

    If I understand correctly, you can buy only one product at a time. After that, the return will be returned. I did not use the machine guns, if I made a mistake - well, sorry.

    The test immediately broke me off - the input interface for the user is digital! We assume that the numbers are from 1. We write the test:

     [TestMethod] public void Buy() { var machine = new VendingMachine(); var beerPrice = new Money() { Euros = 1, Cents = 10 }; var count = 3; var name = "Пиво"; machine.InsertCoin(new Money() { Euros = count }); machine.AddOrUpdateProduct(name, beerPrice, count); var product = machine.Buy(1); Assert.AreEqual(machine.Products[0].Available, count - 1); Assert.AreEqual(product.Name, name); Assert.AreEqual(machine.Amount, default(Money)); } 

    Fuh, outside a simple test raises a bunch of questions. Money is easier to make at least partially working with simple operations:

     public static Money operator +(Money m1, Money m2) { return new Money() { Euros = m1.Euros + m2.Euros, Cents = m1.Cents + m2.Cents }; } public static Money operator -(Money m1, Money m2) { return new Money() { Euros = m1.Euros - m2.Euros, Cents = m1.Cents - m2.Cents }; } public static bool operator <(Money m1, Money m2) { if (m1.Euros != m2.Euros) return m1.Euros < m2.Euros; return m1.Cents < m2.Cents; } public static bool operator >(Money m1, Money m2) { return m2 < m1; } public static bool operator ==(Money m1, Money m2) { return m1.Euros == m2.Euros && m1.Cents == m2.Cents; } public static bool operator !=(Money m1, Money m2) { return !(m1 == m2); } 

    This also does not hurt the product:

     public static Product operator +(Product m1, int m2) { return new Product() { Available = m1.Available + m2, Price = m1.Price, Name = m1.Name }; } public static Product operator -(Product m1, int m2) { return new Product() { Available = m1.Available - m2, Price = m1.Price, Name = m1.Name }; } 

    Then, the purchase looks more or less simple, I really did not understand what product the interface should return - I return the balance in the machine so that it can be displayed for example.

     public Product Buy(int productNumber) { if (products.Count < productNumber || productNumber < 1) throw new IndexOutOfRangeException("Товара нет."); var index = productNumber - 1; var product = products[index]; if (product.Available <= 0) throw new IndexOutOfRangeException("Товара нет."); if (Amount < product.Price) throw new Exception("Не хватает денег."); Amount = Amount - product.Price; ReturnMoney(); product = product - 1; products[index] = product; return product; } 

    So, the third test is now also green. If you think that the code is written easily - I assure you, quick launch of the test makes writing it even easier. All exceptions in the Buy method are written solely due to the test, but even so, I could miss some cases.


    I will not describe the last case - in it all the salt of the tests. We need to write a test, add restrictions and catch the pain of the fall of previous tests that worked with the purchase and the curves of the entered data. This is the whole point of TDD - you wrote yourself the logic of the application developer, then implemented some kind of internal and complex crap, and then got the very pain that everyone who uses your development experiences. Your suffering is much cheaper than the suffering of users, and therefore TDD (which is completely PainDD) helps write tests and a stable product.

    Run the test more often during development - he will immediately say that you have forgotten. When developing methods - do not forget to check the input data and the data of the surrounding world, and then in the tests there is always a desire to give invalid data and get a result. Buy a product without entering the money, request money without adding them, etc. The more real-life case studies you have, the more specific and useful your tests will be; you shouldn’t invent nonsense for testing.

    UPD: a lot of things are missed because they are not needed for tests and because of laziness. Cents do not add up to euros, the goods can only be added, although in reality you need a quantitative addition, the price of the goods cannot be changed. Above that, you need to think that you need to clarify something with a business analyst or domain specialist - there are enough difficulties.

      1. Understanding what you want to create. To outline this
      2. Write test as if the test object has already been written
      3. Run the test . Make sure the test fell. It is important!
      4. Write code in the test object, the code is sufficient to compile
      5. Run the test . Make sure that the test is passed. It is important!
      6. Write code in the test object
      7. Run the test . Make sure that the test is passed. It is important!
      8. Refactor if necessary
      9. Run the test . Make sure that the test is passed. It is important!

      You can not neglect the launch of the test between Etami development. The test must fall where you need to fall. It is for this reason that p.3, p.5, p.7 and p.9 are extremely important!

      After the test object is implemented, you need to make sure that the test:

      • Is in the right set of testing. If this is not the case, then move where it should;
      • Contains only and only one check. There must be only one reason for which the test should fall. In other words, there should be only and only one assert . If not, then use a hamcrest type hamcrest ;
      • It is carried out quickly enough. The main quality of the unit test is the speed of work. If unit tests run slowly, the developer will avoid running them.
      • one
        One assert test is a strong recommendation, not a dogmatic requirement. This restriction makes the tests more understandable, but I do not consider several assert directives in the test a crime if the test as a whole does one test and remains clean. - Ildar Khairullin
      • @ IldarHayrullin: You are mistaken. This is not a recommendation. This is a requirement! There can be no more than one statement in a test! assert this "affirm". A test can only be carried out by checking statements about something. If the test fails, and there are several statements in it, then how to answer the question: “What exactly broke?”. In other words, the programmer will be required to open the test code and see the statements. What for? You can say that there should be a detailed report with stack rates. But why include it? - sys_dev
      • one
        How to know exactly where the failure? Elementary: when triggered, assert produces a diagnostic string. Not all TDD ideologues consider this a requirement for, for example, Kent Beck and Robert Martin. - Ildar Khairullin
      • one
        I agree with @IldarHayrullin: One test means one test case. Each test should check only one test case. Sometimes you need several Assert.Xxx() calls to test a test case, this is normal. Distinguish between them can and should be written in each adequate error message. What not to do is to check several test cases in one test. - andreycha
      • @andreycha: Disagree. One test - one assert ! Translation of this word into Russian "Approved." We cannot check several statements in one test! assert construction checks 1 assumption about the test object, and the test checks exactly one assumption, otherwise these are already tests! We cannot write tests by ourselves, so we design them as functions. They cannot work on their own, and therefore we use assert calls. The programmer should NOT look in the log to clarify what exactly broke, he should understand the cause of the failure from the test name - sys_dev

      As I understand it, you first need to implement a test method and then write it.

      Absolutely right. This is the main idea of ​​developing through testing (TDD). Having defined the interface of a function or class, then you define a set of functional requirements through matching the input and output data. For example, you need to implement a search for the largest of 2 numbers (an example of C ++, but the meaning should be clear)

       template<typename T> T max(const T& a, const T& b); Условный набор тестов: assert(max(1, 4) == 4); assert(max(4, 1) == 4); assert(max(-1, -4) == -1); 

      After that, you start to implement the method and ultimately all tests must pass. Similarly, with the sales terminal class, having defined the interface, you write tests on how the internal representation of the class instance will change when calling its methods with different parameters

      • A little bit wrong. First , the requirements for the program / module / class / method are developed, then they are written in the test language. In the process of writing requirements and coding them in tests, you can create an acceptable interface at the moment. - Ildar Khairullin

      I do not understand how you can write a unit test first, and then implement the method

      This is normal. No one understands this. Without knowing the subject area, without discussing the TOR with the customer and with colleagues, without having much development experience in general and development experience in this subject area in particular, it is impossible to immediately start writing tests.

      Those who claim that you need to immediately write a red test, slightly cunning. No matter how much I talked with such developers, no matter how many examples I saw on the Internet, there is always a test written on an already familiar topic. That is, this developer has participated in at least one project (or, rather, in several for a number of years) in this subject area. And based on his previous years of experience, he is able to write a test first . But in fact, the code was first written: in previous projects.

      And as for the subject matter, the writing of trial pieces of code, their rewriting, throwing out again begins ... It makes no sense to write a test for code that is guaranteed to be thrown out. And only after several iterations, a certain framework crystallizes out, according to which one can begin to write tests.

      Therefore, do not be discouraged when you will not immediately get to write first the tests and only then the code.

      • In fact, this is not quite normal. TDD allows you to minimize the design, but not the development of requirements. Tests are based on requirements. In essence, tests are the requirements in digital form. A test as a declaration of what a method should do, the body of a method is an implementation, how it will do it. - Ildar Khairullin
      • @ IldarHayrullin argue. Tests should verify the contract - input-output pairs. What is between the input and the output, i.e. implementation, they should not check. Otherwise, we risk getting too fragile tests - the implementation tends to change more often than the contract. Tests should check what, but not how. - andreycha
      • @andreycha, that is what I meant (“test as a declaration of what the method should do”). The test expects a certain result from the test method in accordance with the requirements. I didn’t say anything about the binding of the test to the implementation - probably, it was just unclear. - Ildar Khairullin
      • @ IldarHayrullin re-read your comment again and realized that I did not understand you so much :). I apologize. All right, say it. - andreycha