2016-05-09 5 views
1

У меня есть этот красивый пейзаж передо мной, включая JSF, JUnit (4.11) и Mockito (1.10.19):Как издеваются недостижимые свойства класса третьей стороны через Mockito

@ManagedBean 
@ViewScoped 
public class UserAuth implements Serializable { 

private List<UserRole> roleList; 
private LocalChangeBean localChangeBean; 

public UserAuth() { 

     roleList = new ArrayList<UserRole>(); 

     localChangeBean = (LocalChangeBean) FacesContext.getCurrentInstance().getExternalContext().getSessionMap().get("localChangeBean"); 

     setLocalChangeBean(localChangeBean); 

     setRoleList(getLocalChangeBean().getRoleList()); 
     //many other property setting and some JSF stuff 
    } 

public boolean checkAuth() { 
     for (UserRole role : getRoleList()) { 
      if(role.getName().equals("SUPER_USER")) 
       return true; 
     } 
     return false; 
    } 
} 

//A hell of a lot more code, proper getters/setters etc. 

Вот тестовый класс:

public class UserAuthTest { 

    @Test 
    public void testCheckAuth() { 

     UserAuth bean = mock(UserAuth.class); 

     List<UserRole> mockRoleList = new ArrayList<UserRole>(); 
     UserRole ur = mock(UserRole.class); 
     when(ur.getName()).thenReturn("SUPER_USER"); 
     mockRoleList.add(ur); 

     when(bean.getRoleList()).thenReturn(mockRoleList); 

     assertEquals(true, bean.checkAuth()); 
    } 

Дело в том, Класс UserRole недоступен для меня, это еще одна часть проекта. У него нет конструктора без аргументов, и существующий конструктор требует других недостижимых классов и т. Д. Таким образом, я не могу его создать. В этих обстоятельствах все, что я хочу сделать, это заставить этот mock UserRole вести себя так, как возвращать необходимую String, когда вызывается метод getName().

Но, очевидно; когда я пытаюсь добавить этот объект userRole mock в список UserRoles, поведение, которое я пытался определить, не сохраняется вместе с объектом. И да, код выглядит довольно забавно в его текущей позиции. Хотя я оставил его там, чтобы узнать, что я должен сделать, чтобы достичь этой простой, маленькой цели.

Post-Edit: Я не мог решить проблему без изменения исходного компонента, хотя я следовал за предложением Джеффа ниже, и он хорошо работал как стратегия изоляции. Я не отметил это как лучший ответ, так как вопрос был «Как насмехаться над недостижимым классом третьей стороны?» (в текущем примере его класс UserRole) В конце концов, noob я понял, что «Издевательство над недостижимым третьим классом не отличается от насмешек над любым другим классом».

Вот как мне это удалось:

@ManagedBean 
@ViewScoped 
public class UserAuth implements Serializable { 

private List<UserRole> roleList; 
private LocalChangeBean localChangeBean; 

public UserAuth() { 

     //the actual constructor including all JSF logic, highly dependent 
    } 

UserAuth(List<UserRole> roleList) { 
    setRoleList(roleList); 
    //package private test-helper constructor which has no dependency on FacesContext etc. 
    } 
public boolean checkAuth() { 
     for (UserRole role : getRoleList()) { 
      if(role.getName().equals("SUPER_USER")) 
       return true; 
     } 
     return false; 
    } 
} 

А вот тестовый класс (внимание к итератору издеваться, то есть весь фокус):

public class UserAuthTest { 

private UserRole mockRole; 
private Iterator<UserRole> roleIterator; 
private List<UserRole> mockRoleList; 

private UserAuth tester; 

@SuppressWarnings("unchecked") 
@Before 
public void setup() { 

    mockRoleList = mock(List.class); 
    mockRole = mock(UserRole.class); 
    roleIterator = mock(Iterator.class); 

    when(mockRoleList.iterator()).thenReturn(roleIterator); 
    when(roleIterator.hasNext()).thenReturn(true, false); 
    when(roleIterator.next()).thenReturn(mockRole); 

    tester = new UserAuth(mockRoleList); 

} 

@Test 
public void testCheckAuth(){ 
    when(mockRole.getName()).thenReturn("SUPER_USER"); 
    assertEquals("SUPER_USER expected: ", true, tester.checkAuth()); 
} 
+0

Для кода, который не хорошо спроектирован (и не может быть протестирован), может потребоваться использовать еще один инструмент. В Java + Mockito вы можете использовать [PowerMock] (https: // github.ком/jayway/powermock). Это позволяет вам высмеивать конструкторы и другие вещи, к которым обычно не имеют доступа. Просто для того, чтобы объяснить себя по-другому, PowerMock - это сложный инструмент, который должен использоваться только для устаревших проектов (или областей проектов), которые ** не находятся под активным обслуживанием. Причина в том, что это сложная задача и делает тест довольно сложным читать. Если можно, попробуйте реорганизовать 'UserRole', чтобы его можно было протестировать. – Augusto

+0

красивый комментарий для модульного тестирования n00bs, как я. Спасибо. – patateskafa

+1

np! Я стараюсь избегать использования powermock в качестве чумы, поскольку я знаю, что это скользкий склон вниз к плохому дизайну, и для этого требуется время, чтобы оправиться от этого. Надеюсь, вы сможете пересмотреть проект, над которым вы работаете :). – Augusto

ответ

2

Вам не нужен Mockito. Быстрый рефактор сделает это за вас.

Ваша проблема: Ваш код основан на статическом вызове FacesContext.getCurrentInstance() в вашем конструкторе, который трудно подготовить или заменить в тестах.

Ваше предлагаемое решение: Используйте Mockito для замены экземпляра FacesContext, внешнего контекста или карты сеанса. Это отчасти сложно, потому что Mockito работает путем проксирования экземпляров, поэтому без PowerMock вы не сможете заменить статический вызов и без возможности вставить макет в FacesContext или его дерево, у вас нет альтернативы.

Мое предлагаемое решение: Выложить неудачный вызов FacesContext.getCurrentInstance().getExternalContext.getSessionMap() в конструктор по умолчанию. Не вызывайте этот конструктор из тестов; предположим, что он работает в модульном тестировании. Вместо этого напишите конструктор, который принимает карту сеанса как Map<String, Object>, и вызовите этот конструктор из ваших тестов. Это дает вам наилучшую возможность проверить свою собственную логику.

@ManagedBean 
@ViewScoped 
public class UserAuth implements Serializable { 
    // [snip] 
    public UserAuth() { 
    // For the public default constructor, use Faces and delegate to the 
    // package-private constructor. 
    this(FacesContext.getCurrentInstance().getExternalContext().getSessionMap()); 
    } 

    /** Visible for testing. Allows passing in an arbitrary map. */ 
    UserAuth(Map<String, Object> sessionMap) { 
    roleList = new ArrayList<UserRole>(); 

    localChangeBean = (LocalChangeBean) sessionMap.get("localChangeBean"); 

    setLocalChangeBean(localChangeBean); 
    setRoleList(getLocalChangeBean().getRoleList()); 
    // [snip] 
    } 
} 

p.s. Другое решение состоит в том, чтобы фактически получить карту сеанса в рамках теста и вставить необходимое вам значение, но вы должны быть осторожны, чтобы не загрязнять ваши другие тесты, установив что-то в статический экземпляр, который может сохраняться между тестами.

+0

Пакет частных методов или конструкторов делает меня несчастным. Другие классы в одном пакете имеют полный доступ к нему. Я предпочитаю насмешливую структуру. –

+0

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

+0

Джефф благодарит вас так много; но можете ли вы объяснить это более явно? (в интересах низкоуровневых, таких как я), что именно этот частный конструктор пакета предоставит мне, я не мог понять. почему это не публично? – patateskafa

Смежные вопросы