luchob/softuni-feb2023

contextLoads

Closed this issue · 15 comments

Здравей Лъчо,

Имам проблем при стартирането на "void contextLoads", когато пусна всички тестове.

Излизат ми най различни грешки/причини...

  • Failed to load ApplicationContext
  • Caused by: java.lang.IllegalStateException: Failed to execute CommandLineRunner
  • Caused by: java.sql.SQLSyntaxErrorException
  • Error creating bean with name 'credentialServiceImpl'
  • Error creating bean with name 'userServiceImpl'
  • Error creating bean with name 'emailServiceImpl'
  • Error creating bean with name 'javaMailSender'

Но не мога да разбера, какво точно трябва да направя за да го фиксна.

Имам депендънси: testRuntimeOnly 'org.hsqldb:hsqldb'
Сложих и това: testImplementation 'com.icegreen:greenmail:2.0.0' ,но не съм стигнал да го полазвам все още.
Тези също ги имам:

  • testImplementation 'org.springframework.boot:spring-boot-starter-test'
  • testImplementation 'org.springframework.security:spring-security-test'

На този етап имам и това в тестовия recources application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: update
    defer-datasource-initialization: true
  sql:
    init:
      mode: never

Ако може малко насока, когато имаш малко време :)
Това е линк към репото: https://github.com/RosenMitrov/Task-Management-System

Благодаря предварително!

Поздрави,
Росен

luchob commented

Здрасти, Роска!

Видях какви грешки си написал че ти дава, само последната си изпуснал :-)

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'mail.host' in value "${mail.host}"

Както говорихме, при липса на някакви специфични профили application.yaml в тестовете изцяло "покрива" или override-ва истинския application.yaml където имаш подобни пропъртита:

mail:
  host: "localhost"
  port: 1025
  username: "task-management-sytem@tms-group.bg"
  password: ""

Т.е. там ги имаш, а в тестовото yaml ги нямаш => ги нямаш когато се вдига тестовия контекст. Обаче тестовия контекст си е съвсем като истински и това нещо спира да работи и започва да гърми:

@Value("${mail.host}") String mailHost

За експеримент ако си изтриеш мейл настройките от истинкото yaml и се опиташ да пуснеш приложението ще получиш абсолютно същия ексепшън. Сложи dummy стойности и в тестовото yaml:

mail:
  host: localhost
  ....
  ....

После ще си ги ползваш с greenmail.

Поздрави,
Л.

Благодаря много за бързата реакция! Както винаги съм видял всичко без нещото, което чупи всичко .....

Поставих ги ето тук директно и се орпави само да ги взима, не знам дали си пробвал, но се оказа че сработва.

image

Обаче се появи и друг проблем:
Навсякъде в контролерите ползвам @AuthenticationPrincipal AppUserDetails appUserDetails, като AppUserDetails е моето класче, което екстендва User и тн. и се оказва, че нещо не се разбира с @WithMockUser или поне аз не мога да ги накрам :)
Търсейки информация как да го накарам да го взима намерих, че има и @WithUserDetails -> бум и сработи, НО ме притеснява едно нещо и то не малко. Работи само с имейли, които ги имам на ниво MySql, а на практика теста тръгва с hsqldb. Опитах се да напълня мокнато репо в @beforeeach обаче резултата е кръгла нула. На ниво MySql имам точно три ентитита и намира само и единствено тях и това е пълна мистерия.
Тук е наична по който не сработва -> https://github.com/RosenMitrov/Task-Management-System/blob/master/src/test/java/app/taskmanagementsystem/web/TaskControllerIT.java
Ако value = "admin@adminov.bg" го оставя по този начин, него го има в "орогиналната база" и теста върви, но не мисля че това е правилно..

Поздрави,
Росен

luchob commented

Здравей!

Отностно коментар но. 1 - не е необходимо да ги взема в теста... Просто, когато тестовия контекст се вдига той се натъква на този клас:

@Controller
public class MailConfiguration {
    @Bean
    public JavaMailSender javaMailSender(
            @Value("${mail.host}") String mailHost,
            ....
    ) {

       .....
    }
}

И казва - хоп, не знам откъде да взема mail.host. След като си ги добавил в тестовото applicaiton.yaml:

mail:
  host: "localhost"
  port: 1025
  username: "task-management-sytem@tms-group.bg"
  password: ""

То тогава този проблем вече го няма и теста ти спокойно ще мине ето така:

@SpringBootTest
class TaskmanagementsystemApplicationTests {
    @Test
    void contextLoads() {
    }
}

Относно мистерия 2, резултат никога няма да има заради това:

    @Mock
    private UserRepository userRepository;

Това е анотация от моките и не е истинското репозитори. Ти просто си създал някакъв кух обект който не прави нищо и си го викаш в @BeforeEach. Празен обект => нищо. Тъй като е интегрейшън тест идеята е следната:

    @Autowired
    private UserRepository userRepository;

Така ще се inject-не истинското репозитори с връзка към hsql.

Поздрави,
Л.

Не ми се получава и по този начин:

Тест клас:

@SpringBootTest
@AutoConfigureMockMvc
public class TaskControllerIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;

    private UserEntity userEntity;
    private UserRoleEntity userRoleEntity;

    @BeforeEach
    void setUp() {
        userRoleEntity = new UserRoleEntity().setRole(RoleTypeEnum.USER).setDescription("aaaa");
        userRoleRepository.saveAndFlush(userRoleEntity);

        userEntity = new UserEntity()
                .setFirstName("test first")
                .setLastName("set last")
                .setPassword("123")
                .setEnabled(true)
                .setCreatedOn(LocalDateTime.now())
                .setEmail("adminTest@adminov.bg")
                .setUsername("aaa")
                .setRoles(List.of(userRoleEntity));
        this.userRepository.saveAndFlush(userEntity);
    }

    @Test
    @WithMockUser(username = "adminTest@adminov.bg")
    void test_getTasks() throws Exception {
        mockMvc
                .perform(MockMvcRequestBuilders.get("/users/tasks/all"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.view().name("tasks-all"))
                .andExpect(MockMvcResultMatchers.model().attributeExists("allTasksDetailsViews"));
    }
}

Желан контролер и метод за тестване:

@Controller
@RequestMapping("/users/tasks")
public class TaskController {

    private final TaskService taskService;

    @Autowired
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping("/all")
    public String getTasks(@AuthenticationPrincipal AppUserDetails appUserDetails,
                           Model model) {
        List<TaskDetailsViewDto> allTasksDetailsViews = this.taskService.getAllTasksDetailsViews(appUserDetails.getUsername());
        model.addAttribute("allTasksDetailsViews", allTasksDetailsViews);
        return "tasks-all";
    }
}

При този вариант, нито с @WithMockUser нито с @WithUserDetails зацепва. Веднъж ми казва integrity constraint violation: unique constraint or index violation, веднжън че не може да си гетне appUserDetails.getUsername() защото е null и никаква зелена светлина, пробвах може би пак всичко без работещото.....

Ще го мъча някак дано тръгне :)

Благодаря!

Поздрави,
Росен

luchob commented

spring-projects/spring-security#6591

;-)

@WithUserDetails(
        value = "adminTest@adminov.bg",
        setupBefore = TestExecutionEvent.TEST_EXECUTION
    )

👍

@SpringBootTest
@AutoConfigureMockMvc
public class TaskControllerIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;

    private UserEntity userEntity;
    private UserRoleEntity userRoleEntity;

    @BeforeEach
    void setUp() {

        userEntity = new UserEntity()
            .setFirstName("test first")
            .setLastName("set last")
            .setPassword("123")
            .setEnabled(true)
            .setCreatedOn(LocalDateTime.now())
            .setEmail("adminTest@adminov.bg")
            .setUsername("aaa")
            .setRoles(List.of(userRoleRepository.findFirstByRole(RoleTypeEnum.USER).orElseThrow()));

        this.userRepository.saveAndFlush(userEntity);
    }

    @Test
    @WithUserDetails(
        value = "adminTest@adminov.bg",
        setupBefore = TestExecutionEvent.TEST_EXECUTION
    )
    void test_getTasks() throws Exception {
        mockMvc
            .perform(MockMvcRequestBuilders.get("/users/tasks/all"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.view().name("tasks-all"))
            .andExpect(MockMvcResultMatchers.model().attributeExists("allTasksDetailsViews"));
    }
}

Много благодаря Лъчо! Това наистина ще ми спести много време ! :)

Единствено все още имам усещането, че има някаква връзка межеду 2те бази. Направи ми впечатление, че си гетнал ролята от "оригиналната база" -> userRoleRepository.findFirstByRole(RoleTypeEnum.USER).orElseThrow())?
П.С оригинална база наричам това което не е инсъртнато на ниво test папка в проекта. 👍

Иначе това парче код:

UserRoleEntity userRoleEntity = new UserRoleEntity().setRole(RoleTypeEnum.USER).setDescription("aaaa");
        userRoleRepository.saveAndFlush(userRoleEntity);

Предизвиква:

  • could not execute statement; SQL [n/a]; constraint [UK_G50W4R0RU3G9UF6I6FR4KPRO8]

Което мен ме насочва, че каквото е напъхано в оригиналната база MySqlDB това го има и в Hsqldb.
Опитах да запазя и данни с юзъри, които знам, че ги има и отново ми дава този constraint.

Или другото което си мисля е, когато се статира тест в "TaskControllerIT", отделно на ниво main в проекта имам следния код:
https://github.com/RosenMitrov/Task-Management-System/blob/master/src/main/java/app/taskmanagementsystem/init/DatabaseInitialization.java

Дали това цялото нещо се налива в inMemoryDb-то ?? Макар и този Component да не е в тестовата папка, a на ниво main? За проба в класа DatabaseInitialization махнах Component и сякаш на ниво тестване всичко е "празно" и мога да инсърнта отново ролите, или юзъри които подозирах че ги има.

Наистина си мисля, че нещо такова се случва и ми е интересно ти как ги виждаш нещата и дали това наблюдение е правилно?

Също ще потърся вариант как да дебъгна и да видя какво точно се налива в тази временна база.

Още един път искам да изразя балгодарност за всичко, което направи и продължаваш да правиш за нас ! ! !

Поздрави,
Росен

luchob commented

Дали това цялото нещо се налива в inMemoryDb-то ??

Разбира се, че се налива. Имаш съвсем стандартен компонент DatabaseInitialization който си се вдига при вдигането на тестовия контекст.

Поздрави,
Л.

image

Ясно, сега понаредих тази "магия" и от къде идват тези данни :)

Много благодаря!

Поздрави,
Росен

thrako commented

Здравей Лъчо,

Не мога да определя дали проблемът ми е подобен, но мисля, че е, ако ти не мислиш така, ще отворя ново issue. Понеже ползвам Cloundinary и си сетнах env variable, както ни показа, с разликата, че не съм я изнесъл в конфигурационния файл, а си я ползвам директно: преди със System.getenv("CLOUDINARY_URL"), днес пробвах и с @Value("#{environment.getProperty('CLOUDINARY_URL')}"). И по двата начина приложението работи в основната си част src\main\java... и НЕ работи в тестовата част src\test\java..., освен ако не задам env variable преди извикване на съответния метод / клас / all tests или каквото там викам. Веднъж като ги задам си ги пази, но примерно съм сетнал за класа и след това искам да пусна само конкретен метод => гърмеж, докато не задам env variable в настройките при викането на този метод. Четох из Stack Overflow, не намерих нищо смислено, поради което се обръщам към теб.

Та... има ли как да настроя всички тестовете, вкл. всеки клас и всеки метод да си взема CLOUDINARY_URL от "скришно" място?
План "Б" ми е да си направя още една регистрация в Cloudinary за тестови цели и да си я сложа в тестовия application.yml, но ми се струва дърварско решение. Ти какво мислиш?

EDIT: Линк към проекта

Поздрави,
Траян

luchob commented

@thrako Къде си задал променливите, в IntelliJ run конфигурацията?

thrako commented

Да, точно там. Както ни показа на лекцията.
Поздрави,
Траян

luchob commented

@thrako Здрасти!

Всяка "run-ване" в IntelliJ създава конфигурация която може да извикваш многократно.
Например - една за тест1, една за всички тестове, една за приложението и т.н.

Те стартират JVM "на чисто" без да гледат настройките на другите конигурации.
Според мен за тестовете е нормално да си сложиш тестови конфигурации или ако клаудинарито не е предвидено да участва в тестовете има доста опции с @ConditionalOn анотациите. Има и възможност да изключваш бийнове с профили, но мисля че това вече е прекалено :-)

Друга опция е да си сетнеш променливите на ниво OS.

Поздрави от гората,
Л. :-)

thrako commented

Здравей Лъчо,

Според мен за тестовете е нормално да си сложиш тестови конфигурации или ако клаудинарито не е предвидено да участва в тестовете има доста опции с @ConditionalOn анотациите. Има и възможност да изключваш бийнове с профили, но мисля че това вече е прекалено :-)

Мисля, че ще е най-лесно за всички, ако просто си регистрирам втори account в Cloudinary, който да използвам само за тестовете. Не са ми толкова ценни Cloudinary credentials, просто исках да зная как се прави по принцип (по-нататък може да ми се наложи за credentials, които са ми ценни). Добре е да зная за @ConditionalOn, прегледах по диагонал, но не мисля, че в случая ще ми свършат работа, защото CloudUtility-то (@Component) ми е част от RecipeKeeper(също @Component) и участва в качването на рецепти. Сега като го пиша се замислих, че това, което съм направил, никак не е Loose Coupling, ще се рефакторира по-нататък, но оттук до неделя - ще се кара по пътя на най-малкото съпротивление.

Друга опция е да си сетнеш променливите на ниво OS.

Малко ме е страх да пипам там :) Може би след курса, на друга машина ще тествам как се прави. Все пак, благодаря за предложението.

Поздрави от гората,
Л. :-)

Ах... как ми се "хваща гората" и на мен, но няма изгледи скоро да се случи :-) Приятно изкарване!

Поздрави,
Траян

thrako commented

Здравей отново,

Като ти го написах това горното:

ще се кара по пътя на най-малкото съпротивление

... и като ми стана едно тъпо, ама много тъпо - заради някакъв си изпит да отказвам да уча нови неща, та изпитът е един ден, знанията са за цял живот ... и така се ядосах на себе си, та седнах и ги разнищих тези @Conditional анотации, та взеха, че проработиха и така се развихрих, че... дръж се... mock-нах Cloudinary!!! Можеш ли да повярваш? Да де, за теб сигурно е дреболия, но аз съм много горд от себе си и мисля, че стана много яко -> линк.

Та, няма ново issue, само да се похваля и да благодаря, че косвено ме вдъхнови.

Поздрави,
Траян

luchob commented

🤓 😎