Repository: cookiecutter/cookiecutter

Issue:
Prompt list of choices if list in cookiecutter.json
Example:

```
{
    "status_classifier": [
        "Development Status :: 1 - Planning",
        "Development Status :: 2 - Pre-Alpha",
        "Development Status :: 3 - Alpha",
        "Development Status :: 4 - Beta",
        "Development Status :: 5 - Production/Stable",
        "Development Status :: 6 - Mature",
        "Development Status :: 7 - Inactive"
    ],
}
```

... would be prompted like

```
Select status_classifier:
    1 - Development Status :: 1 - Planning
    2 - Development Status :: 2 - Pre-Alpha
    3 - Development Status :: 3 - Alpha
    4 - Development Status :: 4 - Beta
    5 - Development Status :: 5 - Production/Stable
    6 - Development Status :: 6 - Mature
    7 - Development Status :: 7 - Inactive
Enter your choice (1, 2, 3, 4, 5, 6 or 7):
```



Discussion:
I like this idea! Let me bounce it off the other core devs. :bell:  @audreyr @michaeljoseph @hackebrot  

Great idea, indeed! :+1:  Need this for my Kivy templates :grinning: 

Did a quick proof-of-concept using `click.prompt` with [choices](http://click.pocoo.org/4/options/#choice-options) see https://github.com/hackebrot/cookiecutter/commit/3798f0945399edcb115cb09136dbbc73bcf4aa52 :triumph: 

:sparkling_heart: 

Very nice, thank you guys :yum:

:sparkling_heart: :+1: 

:+1: 

@hackebrot in its proof of concept, how would defined the default value of choices in cookiecutter.json?

Will it support multiple selection?


Resolving patch:
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -9,7 +9,7 @@
 """
 
 from __future__ import unicode_literals
-import sys
+from collections import OrderedDict
 
 import click
 
@@ -17,23 +17,90 @@
 from jinja2.environment import Environment
 
 
-def read_response(prompt=''):
-    """Prompt the user and return the entered value or an empty string.
+def read_user_variable(var_name, default_value):
+    """Prompt the user for the given variable and return the entered value
+    or the given default.
 
-    :param str prompt: Text to display to the user
+    :param str var_name: Variable of the context to query the user
+    :param default_value: Value that will be returned if no input happens
+    """
+    # Please see http://click.pocoo.org/4/api/#click.prompt
+    return click.prompt(var_name, default=default_value)
+
+
+def read_user_yes_no(question, default_value):
+    """Prompt the user to reply with 'yes' or 'no' (or equivalent values).
+
+    Note:
+      Possible choices are 'true', '1', 'yes', 'y' or 'false', '0', 'no', 'n'
+
+    :param str question: Question to the user
+    :param default_value: Value that will be returned if no input happens
     """
     # Please see http://click.pocoo.org/4/api/#click.prompt
-    # default: use an empty string if no input happens
-    # prompt_suffix: do not add a suffix to the prompt
-    # show_default: the prompt is expected to mention the default itself
     return click.prompt(
-        prompt,
-        default='',
-        prompt_suffix='',
-        show_default=False,
+        question,
+        default=default_value,
+        type=click.BOOL
     )
 
 
+def read_user_choice(var_name, options):
+    """Prompt the user to choose from several options for the given variable.
+
+    The first item will be returned if no input happens.
+
+    :param str var_name: Variable as specified in the context
+    :param list options: Sequence of options that are available to select from
+    :return: Exactly one item of ``options`` that has been chosen by the user
+    """
+    # Please see http://click.pocoo.org/4/api/#click.prompt
+    if not isinstance(options, list):
+        raise TypeError
+
+    if not options:
+        raise ValueError
+
+    choice_map = OrderedDict(
+        ('{}'.format(i), value) for i, value in enumerate(options, 1)
+    )
+    choices = choice_map.keys()
+    default = '1'
+
+    choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()]
+    prompt = '\n'.join((
+        'Select {}:'.format(var_name),
+        '\n'.join(choice_lines),
+        'Choose from {}'.format(', '.join(choices))
+    ))
+
+    user_choice = click.prompt(
+        prompt, type=click.Choice(choices), default=default
+    )
+    return choice_map[user_choice]
+
+
+def render_variable(env, raw, cookiecutter_dict):
+    if not is_string(raw):
+        raw = str(raw)
+    template = env.from_string(raw)
+    rendered_template = template.render(cookiecutter=cookiecutter_dict)
+    return rendered_template
+
+
+def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input):
+    """Prompt the user which option to choose from the given. Each of the
+    possible choices is rendered beforehand.
+    """
+    rendered_options = [
+        render_variable(env, raw, cookiecutter_dict) for raw in options
+    ]
+
+    if no_input:
+        return rendered_options[0]
+    return read_user_choice(key, rendered_options)
+
+
 def prompt_for_config(context, no_input=False):
     """
     Prompts the user to enter new config, using context as a source for the
@@ -49,55 +116,17 @@ def prompt_for_config(context, no_input=False):
             cookiecutter_dict[key] = raw
             continue
 
-        raw = raw if is_string(raw) else str(raw)
-        val = env.from_string(raw).render(cookiecutter=cookiecutter_dict)
-
-        if not no_input:
-            prompt = '{0} (default is "{1}")? '.format(key, val)
-
-            new_val = read_response(prompt).strip()
+        if isinstance(raw, list):
+            # We are dealing with a choice variable
+            val = prompt_choice_for_config(
+                cookiecutter_dict, env, key, raw, no_input
+            )
+        else:
+            # We are dealing with a regular variable
+            val = render_variable(env, raw, cookiecutter_dict)
 
-            if new_val != '':
-                val = new_val
+            if not no_input:
+                val = read_user_variable(key, val)
 
         cookiecutter_dict[key] = val
     return cookiecutter_dict
-
-
-def query_yes_no(question, default='yes'):
-    """
-    Ask a yes/no question via `read_response()` and return their answer.
-
-    :param question: A string that is presented to the user.
-    :param default: The presumed answer if the user just hits <Enter>.
-        It must be "yes" (the default), "no" or None (meaning
-        an answer is required of the user).
-
-    The "answer" return value is one of "yes" or "no".
-
-    Adapted from
-    http://stackoverflow.com/questions/3041986/python-command-line-yes-no-input
-    http://code.activestate.com/recipes/577058/
-
-    """
-    valid = {'yes': True, 'y': True, 'ye': True, 'no': False, 'n': False}
-    if default is None:
-        prompt = ' [y/n] '
-    elif default == 'yes':
-        prompt = ' [Y/n] '
-    elif default == 'no':
-        prompt = ' [y/N] '
-    else:
-        raise ValueError('Invalid default answer: "{0}"'.format(default))
-
-    while True:
-        sys.stdout.write(question + prompt)
-        choice = read_response().lower()
-
-        if default is not None and choice == '':
-            return valid[default]
-        elif choice in valid:
-            return valid[choice]
-        else:
-            sys.stdout.write('Please respond with "yes" or "no" '
-                             '(or "y" or "n").\n')
diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py
--- a/cookiecutter/vcs.py
+++ b/cookiecutter/vcs.py
@@ -16,7 +16,7 @@
 
 from .compat import which
 from .exceptions import UnknownRepoType, VCSNotInstalled
-from .prompt import query_yes_no
+from .prompt import read_user_yes_no
 from .utils import make_sure_path_exists, rmtree
 
 
@@ -33,11 +33,12 @@ def prompt_and_delete_repo(repo_dir, no_input=False):
     if no_input:
         ok_to_delete = True
     else:
-        ok_to_delete = query_yes_no(
+        question = (
             "You've cloned {0} before. "
-            'Is it okay to delete and re-clone it?'.format(repo_dir),
-            default='yes'
-        )
+            'Is it okay to delete and re-clone it?'
+        ).format(repo_dir)
+
+        ok_to_delete = read_user_yes_no(question, 'yes')
 
     if ok_to_delete:
         rmtree(repo_dir)


Test patch:
diff --git a/tests/test_cookiecutter_local_with_input.py b/tests/test_cookiecutter_local_with_input.py
--- a/tests/test_cookiecutter_local_with_input.py
+++ b/tests/test_cookiecutter_local_with_input.py
@@ -32,8 +32,8 @@ def fin_remove_additional_dirs():
 @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs')
 def test_cookiecutter_local_with_input(monkeypatch):
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'\n'
+        'cookiecutter.prompt.read_user_variable',
+        lambda var, default: default
     )
     main.cookiecutter('tests/fake-repo-pre/', no_input=False)
     assert os.path.isdir('tests/fake-repo-pre/{{cookiecutter.repo_name}}')
@@ -49,8 +49,8 @@ def test_cookiecutter_input_extra_context(monkeypatch):
     `Call cookiecutter()` with `no_input=False` and `extra_context`
     """
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'\n'
+        'cookiecutter.prompt.read_user_variable',
+        lambda var, default: default
     )
     main.cookiecutter(
         'tests/fake-repo-pre',
diff --git a/tests/test_cookiecutter_repo_arg.py b/tests/test_cookiecutter_repo_arg.py
--- a/tests/test_cookiecutter_repo_arg.py
+++ b/tests/test_cookiecutter_repo_arg.py
@@ -38,8 +38,10 @@ def fin_remove_additional_folders():
 @skipif_no_network
 @pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
 def test_cookiecutter_git(monkeypatch):
-    monkeypatch.setattr('cookiecutter.prompt.read_response', lambda x=u'': u'')
-
+    monkeypatch.setattr(
+        'cookiecutter.prompt.read_user_variable',
+        lambda var, default: default
+    )
     main.cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git')
     clone_dir = os.path.join(
         os.path.expanduser('~/.cookiecutters'),
@@ -54,7 +56,10 @@ def test_cookiecutter_git(monkeypatch):
 @skipif_no_network
 @pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
 def test_cookiecutter_mercurial(monkeypatch):
-    monkeypatch.setattr('cookiecutter.prompt.read_response', lambda x=u'': u'')
+    monkeypatch.setattr(
+        'cookiecutter.prompt.read_user_variable',
+        lambda var, default: default
+    )
 
     main.cookiecutter('https://bitbucket.org/pokoli/cookiecutter-trytonmodule')
     clone_dir = os.path.join(
diff --git a/tests/test_prompt.py b/tests/test_prompt.py
--- a/tests/test_prompt.py
+++ b/tests/test_prompt.py
@@ -6,21 +6,6 @@
 -----------
 
 Tests for `cookiecutter.prompt` module.
-TestPrompt.test_prompt_for_config_simple
-TestPrompt.test_prompt_for_config_unicode
-TestPrompt.test_unicode_prompt_for_config_unicode
-TestPrompt.test_unicode_prompt_for_default_config_unicode
-TestPrompt.test_unicode_prompt_for_templated_config
-
-TestQueryAnswers.test_query_y
-TestQueryAnswers.test_query_ye
-TestQueryAnswers.test_query_yes
-TestQueryAnswers.test_query_n
-
-TestQueryDefaults.test_query_y_none_default
-TestQueryDefaults.test_query_n_none_default
-TestQueryDefaults.test_query_no_default
-TestQueryDefaults.test_query_bad_default
 """
 
 from collections import OrderedDict
@@ -28,7 +13,31 @@
 
 import pytest
 
-from cookiecutter import prompt
+from cookiecutter import prompt, compat
+from jinja2.environment import Environment
+
+
+@pytest.mark.parametrize('raw_var, rendered_var', [
+    (1, '1'),
+    (True, 'True'),
+    ('foo', 'foo'),
+    ('{{cookiecutter.project}}', 'foobar')
+])
+def test_convert_to_str(mocker, raw_var, rendered_var):
+    env = Environment()
+    from_string = mocker.patch(
+        'cookiecutter.prompt.Environment.from_string',
+        wraps=env.from_string
+    )
+    context = {'project': 'foobar'}
+
+    result = prompt.render_variable(env, raw_var, context)
+    assert result == rendered_var
+
+    # Make sure that non str variables are conerted beforehand
+    if not compat.is_string(raw_var):
+        raw_var = str(raw_var)
+    from_string.assert_called_once_with(raw_var)
 
 
 @pytest.fixture(autouse=True)
@@ -40,8 +49,8 @@ def patch_readline_on_win(monkeypatch):
 class TestPrompt(object):
     def test_prompt_for_config_simple(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'Audrey Roy'
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: u'Audrey Roy'
         )
         context = {'cookiecutter': {'full_name': 'Your Name'}}
 
@@ -50,8 +59,8 @@ def test_prompt_for_config_simple(self, monkeypatch):
 
     def test_prompt_for_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'Pizzä ïs Gööd'
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: u'Pizzä ïs Gööd'
         )
         context = {'cookiecutter': {'full_name': 'Your Name'}}
 
@@ -60,8 +69,8 @@ def test_prompt_for_config_unicode(self, monkeypatch):
 
     def test_unicode_prompt_for_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'Pizzä ïs Gööd'
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: u'Pizzä ïs Gööd'
         )
         context = {'cookiecutter': {'full_name': u'Řekni či napiš své jméno'}}
 
@@ -70,8 +79,8 @@ def test_unicode_prompt_for_config_unicode(self, monkeypatch):
 
     def test_unicode_prompt_for_default_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'\n'
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: default
         )
         context = {'cookiecutter': {'full_name': u'Řekni či napiš své jméno'}}
 
@@ -80,8 +89,8 @@ def test_unicode_prompt_for_default_config_unicode(self, monkeypatch):
 
     def test_unicode_prompt_for_templated_config(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'\n'
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: default
         )
         context = {'cookiecutter': OrderedDict([
             (
@@ -101,8 +110,8 @@ def test_unicode_prompt_for_templated_config(self, monkeypatch):
 
     def test_dont_prompt_for_private_context_var(self, monkeypatch):
         monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x: pytest.fail(
+            'cookiecutter.prompt.read_user_variable',
+            lambda var, default: pytest.fail(
                 'Should not try to read a response for private context var'
             )
         )
@@ -111,52 +120,129 @@ def test_dont_prompt_for_private_context_var(self, monkeypatch):
         assert cookiecutter_dict == {'_copy_without_render': ['*.html']}
 
 
-class TestQueryAnswers(object):
-    @pytest.fixture(params=[u'y', u'ye', u'yes'])
-    def patch_read_response(self, request, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': request.param
+class TestReadUserChoice(object):
+    def test_should_invoke_read_user_choice(self, mocker):
+        prompt_choice = mocker.patch(
+            'cookiecutter.prompt.prompt_choice_for_config',
+            wraps=prompt.prompt_choice_for_config
         )
 
-    @pytest.mark.usefixtures('patch_read_response')
-    def test_query(self):
-        assert prompt.query_yes_no('Blah?')
+        read_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
+        read_choice.return_value = 'all'
 
-    def test_query_n(self, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'n'
-        )
-        assert not prompt.query_yes_no('Blah?')
+        read_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
 
+        CHOICES = ['landscape', 'portrait', 'all']
+        CONTEXT = {
+            'cookiecutter': {
+                'orientation': CHOICES
+            }
+        }
 
-class TestQueryDefaults(object):
-    def test_query_y_none_default(self, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'y'
-        )
-        assert prompt.query_yes_no('Blah?', default=None)
+        cookiecutter_dict = prompt.prompt_for_config(CONTEXT)
 
-    def test_query_n_none_default(self, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'n'
+        assert not read_variable.called
+        assert prompt_choice.called
+        read_choice.assert_called_once_with('orientation', CHOICES)
+        assert cookiecutter_dict == {'orientation': 'all'}
+
+    def test_should_not_invoke_read_user_variable(self, mocker):
+        read_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
+        read_variable.return_value = u'Audrey Roy'
+
+        prompt_choice = mocker.patch(
+            'cookiecutter.prompt.prompt_choice_for_config'
         )
-        assert not prompt.query_yes_no('Blah?', default=None)
 
-    def test_query_no_default(self, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u''
+        read_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
+
+        CONTEXT = {'cookiecutter': {'full_name': 'Your Name'}}
+
+        cookiecutter_dict = prompt.prompt_for_config(CONTEXT)
+
+        assert not prompt_choice.called
+        assert not read_choice.called
+        read_variable.assert_called_once_with('full_name', 'Your Name')
+        assert cookiecutter_dict == {'full_name': u'Audrey Roy'}
+
+    def test_should_render_choices(self, mocker):
+        read_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
+        read_choice.return_value = u'anewproject'
+
+        read_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
+        read_variable.return_value = u'A New Project'
+
+        RENDERED_CHOICES = [
+            u'foo',
+            u'anewproject',
+            u'bar'
+        ]
+
+        CONTEXT = {'cookiecutter': OrderedDict([
+            (
+                'project_name',
+                u'A New Project'
+            ), (
+                'pkg_name',
+                [
+                    u'foo',
+                    u'{{ cookiecutter.project_name|lower|replace(" ", "") }}',
+                    u'bar'
+                ]
+            )
+        ])}
+
+        EXP_COOKIECUTTER_DICT = {
+            'project_name': u'A New Project', 'pkg_name': u'anewproject'
+        }
+        cookiecutter_dict = prompt.prompt_for_config(CONTEXT)
+
+        read_variable.assert_called_once_with('project_name', u'A New Project')
+        read_choice.assert_called_once_with('pkg_name', RENDERED_CHOICES)
+        assert cookiecutter_dict == EXP_COOKIECUTTER_DICT
+
+
+class TestPromptChoiceForConfig(object):
+    @pytest.fixture
+    def choices(self):
+        return ['landscape', 'portrait', 'all']
+
+    @pytest.fixture
+    def context(self, choices):
+        return {
+            'cookiecutter': {
+                'orientation': choices
+            }
+        }
+
+    def test_should_return_first_option_if_no_input(
+            self, mocker, choices, context):
+        read_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
+
+        expected_choice = choices[0]
+
+        actual_choice = prompt.prompt_choice_for_config(
+            context,
+            Environment(),
+            'orientation',
+            choices,
+            True  # Suppress user input
         )
-        assert not prompt.query_yes_no('Blah?', default='no')
+        assert not read_choice.called
+        assert expected_choice == actual_choice
 
-    def test_query_bad_default(self, monkeypatch):
-        monkeypatch.setattr(
-            'cookiecutter.prompt.read_response',
-            lambda x=u'': u'junk'
+    def test_should_read_userchoice(self, mocker, choices, context):
+        read_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
+        read_choice.return_value = 'all'
+
+        expected_choice = 'all'
+
+        actual_choice = prompt.prompt_choice_for_config(
+            context,
+            Environment(),
+            'orientation',
+            choices,
+            False  # Ask the user for input
         )
-        with pytest.raises(ValueError):
-            prompt.query_yes_no('Blah?', default='yn')
+        read_choice.assert_called_once_with('orientation', choices)
+        assert expected_choice == actual_choice
diff --git a/tests/test_read_response.py b/tests/test_read_response.py
deleted file mode 100755
--- a/tests/test_read_response.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-test_read_response
-------------------
-"""
-
-from __future__ import unicode_literals
-
-from cookiecutter.prompt import read_response
-
-PROMPT = 'project_name (default is "Kivy Project")?'
-
-
-def _monkey_prompt(prompt, **kwargs):
-    expected_kwargs = {
-        'default': '',
-        'prompt_suffix': '',
-        'show_default': False
-    }
-    assert prompt == PROMPT
-    assert kwargs == expected_kwargs
-    return 'Hello World'
-
-
-def test_click_invocation(capsys, monkeypatch):
-    monkeypatch.setattr('click.prompt', _monkey_prompt)
-    assert read_response(PROMPT) == 'Hello World'
diff --git a/tests/test_read_user_choice.py b/tests/test_read_user_choice.py
new file mode 100644
--- /dev/null
+++ b/tests/test_read_user_choice.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+import click
+import pytest
+
+from cookiecutter.prompt import read_user_choice
+
+OPTIONS = ['hello', 'world', 'foo', 'bar']
+
+
+EXPECTED_PROMPT = """Select varname:
+1 - hello
+2 - world
+3 - foo
+4 - bar
+Choose from 1, 2, 3, 4"""
+
+
+@pytest.mark.parametrize('user_choice, expected_value', enumerate(OPTIONS, 1))
+def test_click_invocation(mocker, user_choice, expected_value):
+    choice = mocker.patch('click.Choice')
+    choice.return_value = click.Choice(OPTIONS)
+
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = '{}'.format(user_choice)
+
+    assert read_user_choice('varname', OPTIONS) == expected_value
+
+    prompt.assert_called_once_with(
+        EXPECTED_PROMPT,
+        type=click.Choice(OPTIONS),
+        default='1'
+    )
+
+
+def test_raise_if_options_is_not_a_non_empty_list():
+    with pytest.raises(TypeError):
+        read_user_choice('foo', 'NOT A LIST')
+
+    with pytest.raises(ValueError):
+        read_user_choice('foo', [])
diff --git a/tests/test_read_user_variable.py b/tests/test_read_user_variable.py
new file mode 100755
--- /dev/null
+++ b/tests/test_read_user_variable.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+"""
+test_read_user_variable
+-----------------------
+"""
+
+from __future__ import unicode_literals
+
+import click
+
+from cookiecutter.prompt import read_user_variable
+
+VARIABLE = 'project_name'
+DEFAULT = 'Kivy Project'
+
+
+def test_click_invocation(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = DEFAULT
+
+    assert read_user_variable(VARIABLE, DEFAULT) == DEFAULT
+
+    click.prompt.assert_called_once_with(VARIABLE, default=DEFAULT)
diff --git a/tests/test_read_user_yes_no.py b/tests/test_read_user_yes_no.py
new file mode 100644
--- /dev/null
+++ b/tests/test_read_user_yes_no.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+
+"""
+test_read_user_yes_no
+---------------------
+"""
+
+from __future__ import unicode_literals
+
+import click
+
+from cookiecutter.prompt import read_user_yes_no
+
+QUESTION = 'Is it okay to delete and re-clone it?'
+DEFAULT = 'y'
+
+
+def test_click_invocation(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = DEFAULT
+
+    assert read_user_yes_no(QUESTION, DEFAULT) == DEFAULT
+
+    click.prompt.assert_called_once_with(
+        QUESTION,
+        default=DEFAULT,
+        type=click.BOOL
+    )
diff --git a/tests/test_vcs_prompt.py b/tests/test_vcs_prompt.py
--- a/tests/test_vcs_prompt.py
+++ b/tests/test_vcs_prompt.py
@@ -33,8 +33,8 @@ def remove_cookiecutter_dirs():
 @skipif_no_network
 def test_git_clone_overwrite(monkeypatch):
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'y'
+        'cookiecutter.vcs.read_user_yes_no',
+        lambda question, default: True
     )
     repo_dir = vcs.clone(
         'https://github.com/audreyr/cookiecutter-pypackage.git'
@@ -56,8 +56,8 @@ def test_git_clone_overwrite_with_no_prompt():
 @skipif_no_network
 def test_git_clone_cancel(monkeypatch):
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'n'
+        'cookiecutter.vcs.read_user_yes_no',
+        lambda question, default: False
     )
 
     with pytest.raises(SystemExit):
@@ -67,8 +67,8 @@ def test_git_clone_cancel(monkeypatch):
 @skipif_no_network
 def test_hg_clone_overwrite(monkeypatch):
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'y'
+        'cookiecutter.vcs.read_user_yes_no',
+        lambda question, default: True
     )
     repo_dir = vcs.clone(
         'https://bitbucket.org/pokoli/cookiecutter-trytonmodule'
@@ -80,8 +80,8 @@ def test_hg_clone_overwrite(monkeypatch):
 @skipif_no_network
 def test_hg_clone_cancel(monkeypatch):
     monkeypatch.setattr(
-        'cookiecutter.prompt.read_response',
-        lambda x=u'': u'n'
+        'cookiecutter.vcs.read_user_yes_no',
+        lambda question, default: False
     )
 
     with pytest.raises(SystemExit):