Тестовое задание на java по написанию юнит тестов. Нужно ревью

Добрый день! Недавно мне прислали тестовое задание на должность автотестера джун +. Что-то накодил, отправил работодателю. Пока ответа нет, но интересно Ваше мнение. Вот собственно задание:

Implement a solution to the following problem in Java. We are looking for clean, well-factored, OO code.
You do not need to provide any form of persistence in this program. Your project should contain some way of running automated tests to prove it works, whether you use jUnit or some other way is up to you.
The program should be an API. You can opt to put a user interface on it or not, but we will only be looking at the API portion.

Here are the requirements:
Consider a grocery market where items have prices per unit but also volume prices. For example, doughnuts may be $1.25 each or 3 for $3 dollars.
Implement a point-of-sale scanning API that accepts an arbitrary ordering of products (similar to what would happen when actually at a checkout line) then returns the correct total price for an entire shopping cart based on the per unit prices or the volume prices as applicable.

Here are the products listed by code and the prices to use (there is no sales tax):
Product Code Price
A $1.25 each or 3 for $3.00
B $4.25
C $1.00 or $5 for a six pack
D $0.75

The interface at the top level PointOfSaleTerminal service object should look something like this. You are free to design/implement the rest of the code however you wish, including how you specify the prices in the system:
PointOfSaleTerminal terminal = new PointOfSaleTerminal(); terminal.setPricing(…); terminal.scan(“A”);
terminal.scan(“C”); … etc.
BigDecimal result = terminal.calculateTotal();
Here are the minimal inputs you should use for your test cases. These test cases must be shown to work in your program: Scan these items in this order: ABCDABA; Verify the total price is $13.25. Scan these items in this order: CCCCCCC; Verify the total price is $6.00. Scan these items in this order: ABCD; Verify the total price is $7.25

И вот мое решение:

            public class PointOfSaleTerminal {
    	
    	public String productList(String s) {
    
    		
    		char[] productList = s.toCharArray();
    
    		// считаем количество а, b, с и d
    
    		int numberA = 0;
    		int numberB = 0;
    		int numberC = 0;
    		int numberD = 0;
    
    		for (int x = 0; x < productList.length; x++) {
    
    			if (productList[x] == 'A')
    				numberA++;
    			if (productList[x] == 'B')
    				numberB++;
    			if (productList[x] == 'C')
    				numberC++;
    			if (productList[x] == 'D')
    				numberD++;
    
    			// создаем исключения для несуществующего товара
    
    			if (productList[x] != 'A') {
    				if (productList[x] != 'B') {
    					if (productList[x] != 'C') {
    						if (productList[x] != 'D') {
    							System.out.println("Данного товара нет в списке");
    						}
    					}
    				}
    			}
    		}
    
    		
    
    		// Остаток от деления количества а на 3
    		int restA = numberA % 3;
    		// Остаток от деления количества c на 6
    		int restC = numberC % 6;
    
    		
    
    		// cчитаем стоимость всех товаров по наименованиям
    
    		double costA = ((numberA - restA) / 3) * 3 + (restA) * 1.25;
    		double costB = numberB * 4.25;
    		double costC = ((numberC - restC) / 6) * 5 + restC * 1;
    		double costD = numberD * 0.75;
    
    		// получаем общий результат
    		double TotalResult = costA + costB + costC + costD;
    
    		
    		String str;
    
    		str = Double.toString(TotalResult);
    
    		return str;
    	}
    
    }
    import org.junit.*;


     public class Testing {


	@Test
	public void Test() {
		PointOfSaleTerminal terminal = new PointOfSaleTerminal();
		String list = terminal.productList("ABCDABA");
		Assert.assertEquals(list, "13.25");

	}

	@Test
	public void Test1() {
		PointOfSaleTerminal terminal = new PointOfSaleTerminal();
		String list = terminal.productList("CCCCCCC");
		Assert.assertEquals(list, "6.0");

	}

	@Test
	public void Test2() {
		PointOfSaleTerminal terminal = new PointOfSaleTerminal();
		String list = terminal.productList("ABCD");
		Assert.assertEquals(list, "7.25");


	}

}

Мне понятно, что если бы я шел на дева, то пришлось бы создавать кучу объектов с замысловатой логикой, но как для тестировщика вроде и этого должно хватить. Надеюсь на Ваши советы по улучшению кода.

if можно вывести с swith и тогда избавится от этого, вынеся println в default

  1. Не мешало бы задание сформулировать на русском и покороче
  2. Реализация с использованием Map должна быть поприятнее
1 лайк

Дело в том, что любая автоматизации без

рано или поздно превратится в Адъ и Израиль. А показать эти навыки вас “попросили” через реализацию api. Не хочу расстраивать - но у вас это не получилось.

2 лайка

помогите разобраться. какие объекты должны присутствовать и что они должны реализовывать?

Классное задание, где такие дают? :slight_smile:

Имхо, сдесь видно несколько компонентов.
Сервер - Склад-Магазин, назовем его GroceryStore
Клиент - PointOfSaleTerminal

GroceryStore, хранит состояние, но т.к. персистентность не нужна, то он по факту хранит только цены и бесконечное число товаров.
PointOfSaleTeminal общается с GroceryStore, путем атомарных операций самого API, scan(x) и calculateTotal()… что должно делать setPricing - не понятно. Для атомарного взаимодействия создадим Session, в перспективе чекаут товаров сессии еще и проверяет наличие товара в самом магазине. (хотя с учетом что это гросери, врядли имея на руках товар скажут “извините этот товар закончился”)

В принципе у GroceryStore может быть несколько терминалов. (иметь магазин с одной кассой не очень удобно)

//scala-like code, compatible with JVM

object GroceryStore {
  def connect(): Session = new Session()
  
  //todo: it is better for prices use Currency type...

  val prices = {
    Map("A"-> 1.25, "B" -> 4.25, "C" -> 1, "D" -> 0.75)
  }
  val discounts = {
    Map("A" -> (3->3), "C" -> (6->5)) //this is first what came to my mind :(, map of maps is not clear and really not useful for end user.
  }
  val sum(products) = {
   products.map(???) //wierd logic to calculate sum
  }

}
class Session {
  //Session works on Server side
  private var products:mutable.Map[String, Int] = Map()
  def scan(product:String) = {
    val count = products.get(product).orElse(0)
    products ++= Map(produc -> count+1) //these to lines create or update Map item
  }
  def calculateTotal = {
    GroceryStore.sum(products)
  }
}

class PointOfSaleTerminal {
  //each new customer has its own
  val session = GroceyStore.connect()
  def scan(product:String) = {
    session.scan(product)
  }
  def calculateTotal = session.calculateTotal
}

дальше пишем тесты (ну или наоборот TDD, ATDD все дела):

  1. моки что PointOfSale делегирует вызовы сессии. моки что сессия делегирует сумирование магазину с подстановкой продукта
    … tbd
  2. базовые кейсы, которые были предложены клиентом для GroceryStore или какие посчитаем нужными, лучше price и дискоунт мокать, с тем расчетом что в реалии они идут из персистетного хранилища.
    … tbd
  3. Интеграционные тесты

class PointOfSaleIntegrationTest extends ScalaTest.FreeSpec{
  class Fixture {
    //this fixture converts string of products to scan actions
    val toScan:String //abstract
    val expectedPrice:BigInteger //abstract
    val terminal = new PointOfSaleTerminal()
    toScan.toChars.foreach(terminal.scan(_))
    expectedPrice should be (terminal.calculateTotal)
  }
  "ABCDABA costs 13.25" in new Fixture {
    val toScan = "ABCDABA"
    val expectedPrice = 13.25
  }
  "CCCCCCC costs 6.0" in new Fixture {
    val toScan = "CCCCCCC"
    val expectedPrice = 6
  }
  "ABCD costs 7.25" in new Fixture {
    val toScan = "ABCD"
    val expectedPrice = 7.25
  }
}

P.S. Я не понял зачем вы конвертируете дабл в стринг, это может быть не безопасно и под некоторыми локалями ваш тест просто упадет.

P.P.S Пересмотрел ответ, прошу прощение за scala (jvm быстрее мне на нем на бросать), все мэпится на чистую java. Для облегчения чтения val - константы; типизация есть - просто не нужно явно везде писать; object - это встроенная нотация для синглтона; return неявно есть везде, возвращает последнее выражаение. Тесты по факту создают абстрактные классы, и вызов происходи в “конструкторе” (частый прием для скалы).

спасибо огромное за развернутый ответ. буду разбираться.
p.s. данное задание было в DIO-soft

на вход в данный класс поступает стринг, соответственно и на выходе должен быть он же. или я что-то путаю?

Как сделали, так и возвращает. Херак, и возвращает дабл.

public class PointOfSaleTerminal {
    public double productList(String s) {
                //...
    		double TotalResult = costA + costB + costC + costD;
                return TotalResult;
    }
}

Note:
costA + costB + costC + costD - представьте что товаров у вас миллион.

Успехов! Спрашивайте, если что. Но синтаксис java не мое.

Великий Гугл привел именно на них. :slight_smile:

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

Но ещё чистый код подразумевает отсутствие комментариев (за редкими исключениями), наличие понятных имён и короткие функции. Попробуйте удалить ваши комментарии и выразить ту же информацию в именах переменных и функций. Переименовать “Test”, “Test1” и “Test2” в названия, говорящие о том, что же там тестируется. Уменьшить размер IF, например, с помощью else или switch. Если кода станет в четыре раза меньше - хорошо.

3 лайка

Так а как в результате, предложили офер ?

с оффером по данному заданию не сложилось

да, да, это дио-софт. И собеседует дибил, который считает себя мега-гуру. Я честно говоря рад был, когда попал в другую компанию и не пришлось работать с человеком, который там собеседовал меня.

Непонятно, почему делал и что не так - по-моему, задание классное.

1 лайк

Задание классное, для джуна девелопера, который должен уметь применять ООП и писать тесты на свой код.
Заставлять QA Automation писать юниттесты для чужого говно-кода это конечно хорошо, но писать их должен сам девелопер, имхо.

З.Ы. Скала - зло злющее

А, ну да, это задание для девелопера, конечно.

1 лайк

Полностью согласен. Но есть и такая штука, как “development in test” - вполне жизнеспособная концепция. В текущей конторе, как-то вводили такую штуку. Правда, на данный момент заглохло, разрабы сами юнит-тесты клепают…